├── .github
├── ISSUE_TEMPLATE
│ ├── bug_request.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── README.md
├── client
├── .babelrc.json
├── .eslintrc.json
├── .prettierrc.json
├── .storybook
│ ├── main.js
│ └── preview.js
├── README.md
├── env
│ └── .env.sample
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── imgs
│ │ ├── channel-icon.png
│ │ ├── close-filled-icon.png
│ │ ├── close-icon.png
│ │ ├── detail-icon.png
│ │ ├── emoji-icon.png
│ │ ├── filter-icon.png
│ │ ├── index.d.ts
│ │ ├── lock-icon.png
│ │ ├── logo-text.png
│ │ ├── logo.png
│ │ ├── option-icon.png
│ │ ├── plus-icon.png
│ │ ├── search-icon.png
│ │ ├── send-message-icon.png
│ │ ├── sort-icon.png
│ │ ├── star-blue.png
│ │ ├── star.png
│ │ ├── thread-icon.png
│ │ └── user-icon.png
│ └── index.html
├── src
│ ├── common
│ │ ├── constants
│ │ │ ├── chat-type.ts
│ │ │ ├── chatroom-type.ts
│ │ │ ├── default-section-name.ts
│ │ │ ├── http-status-code.ts
│ │ │ ├── index.ts
│ │ │ ├── key-code.ts
│ │ │ ├── scroll-event-type.ts
│ │ │ ├── size.ts
│ │ │ └── sort-method.ts
│ │ ├── socket
│ │ │ ├── emits
│ │ │ │ ├── chatroom.ts
│ │ │ │ ├── message.ts
│ │ │ │ ├── reaction.ts
│ │ │ │ └── thread.ts
│ │ │ ├── socketIO.ts
│ │ │ └── types
│ │ │ │ ├── chatroom-types.ts
│ │ │ │ ├── message-types.ts
│ │ │ │ ├── reaction-types.ts
│ │ │ │ └── thread-types.ts
│ │ ├── store
│ │ │ ├── actions
│ │ │ │ ├── channel-action.ts
│ │ │ │ ├── chatroom-action.ts
│ │ │ │ ├── modal-action.ts
│ │ │ │ ├── thread-action.ts
│ │ │ │ └── user-action.ts
│ │ │ ├── index.ts
│ │ │ ├── reducers
│ │ │ │ ├── channel-reducer.ts
│ │ │ │ ├── chatroom-reducer.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── modal-reducer.ts
│ │ │ │ ├── thread-reducer.ts
│ │ │ │ └── user-reducer.ts
│ │ │ ├── sagas
│ │ │ │ ├── channel-saga.ts
│ │ │ │ ├── chatroom-saga.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── thread-saga.ts
│ │ │ │ └── user-saga.ts
│ │ │ └── types
│ │ │ │ ├── channel-types.ts
│ │ │ │ ├── chatroom-types.ts
│ │ │ │ ├── message-types.ts
│ │ │ │ ├── modal-types.ts
│ │ │ │ ├── reactions-type.ts
│ │ │ │ ├── thread-types.ts
│ │ │ │ └── user-types.ts
│ │ ├── theme
│ │ │ ├── color.ts
│ │ │ └── index.ts
│ │ └── utils
│ │ │ ├── api.ts
│ │ │ ├── auth.ts
│ │ │ ├── blockPage.ts
│ │ │ ├── index.ts
│ │ │ ├── logout.ts
│ │ │ ├── modal.ts
│ │ │ ├── registerToken.ts
│ │ │ ├── scroll.ts
│ │ │ ├── time.ts
│ │ │ └── uriParser.ts
│ ├── components
│ │ ├── atoms
│ │ │ ├── ActiveLight
│ │ │ │ ├── ActiveLight.stories.tsx
│ │ │ │ └── ActiveLight.tsx
│ │ │ ├── Button
│ │ │ │ ├── Button.stories.tsx
│ │ │ │ └── Button.tsx
│ │ │ ├── DropMenuBox
│ │ │ │ ├── DropMenuBox.stories.tsx
│ │ │ │ └── DropMenuBox.tsx
│ │ │ ├── DropMenuItem
│ │ │ │ ├── DropMenuItem.stories.tsx
│ │ │ │ └── DropMenuItem.tsx
│ │ │ ├── Emoji
│ │ │ │ ├── Emoji.stories.tsx
│ │ │ │ └── Emoji.tsx
│ │ │ ├── HoverInput
│ │ │ │ ├── HoverInput.stories.tsx
│ │ │ │ └── HoverInput.tsx
│ │ │ ├── Icon
│ │ │ │ ├── Icon.stories.tsx
│ │ │ │ └── Icon.tsx
│ │ │ ├── Input
│ │ │ │ ├── Input.stories.tsx
│ │ │ │ └── Input.tsx
│ │ │ ├── LogoImg
│ │ │ │ ├── LogoImg.stories.tsx
│ │ │ │ └── LogoImg.tsx
│ │ │ ├── ModalBox
│ │ │ │ ├── ModalBox.stories.tsx
│ │ │ │ └── ModalBox.tsx
│ │ │ ├── ProfileImg
│ │ │ │ ├── ProfileImg.stories.tsx
│ │ │ │ └── ProfileImg.tsx
│ │ │ ├── ProfileModalBody
│ │ │ │ ├── ProfileModalBody.stories.tsx
│ │ │ │ └── ProfileModalBody.tsx
│ │ │ ├── ProfileModalImg
│ │ │ │ ├── ProfileModalImg.stories.tsx
│ │ │ │ └── ProfileModalImg.tsx
│ │ │ ├── Text
│ │ │ │ ├── Text.stories.tsx
│ │ │ │ └── Text.tsx
│ │ │ └── index.ts
│ │ ├── molecules
│ │ │ ├── Actionbar
│ │ │ │ ├── Actionbar.stories.tsx
│ │ │ │ └── Actionbar.tsx
│ │ │ ├── ActiveProfileImg
│ │ │ │ ├── ActiveProfileImg.stories.tsx
│ │ │ │ └── ActiveProfileImg.tsx
│ │ │ ├── AddChannelButton
│ │ │ │ ├── AddChannelButton.stories.tsx
│ │ │ │ └── AddChannelButton.tsx
│ │ │ ├── BrowsePageChannelBody
│ │ │ │ ├── BrowsePageChannelBody.stories.tsx
│ │ │ │ └── BrowsePageChannelBody.tsx
│ │ │ ├── BrowsePageChannelButton
│ │ │ │ ├── BrowsePageChannelButton.stories.tsx
│ │ │ │ └── BrowsePageChannelButton.tsx
│ │ │ ├── BrowsePageChannelHeader
│ │ │ │ ├── BrowsePageChannelHeader.stories.tsx
│ │ │ │ └── BrowsePageChannelHeader.tsx
│ │ │ ├── BrowsePageControls
│ │ │ │ ├── BrowsePageControls.stories.tsx
│ │ │ │ └── BrowsePageControls.tsx
│ │ │ ├── BrowsePageSearchBar
│ │ │ │ ├── BrowsePageSearchBar.stories.tsx
│ │ │ │ └── BrowsePageSearchBar.tsx
│ │ │ ├── Channel
│ │ │ │ ├── Channel.stories.tsx
│ │ │ │ └── Channel.tsx
│ │ │ ├── ChannelModal
│ │ │ │ ├── ChannelModal.stories.tsx
│ │ │ │ └── ChannelModal.tsx
│ │ │ ├── DM
│ │ │ │ ├── DM.stories.tsx
│ │ │ │ └── DM.tsx
│ │ │ ├── EmojiBox
│ │ │ │ ├── EmojiBox.stories.tsx
│ │ │ │ └── EmojiBox.tsx
│ │ │ ├── EmojiPicker
│ │ │ │ ├── EmojiPicker.stories.tsx
│ │ │ │ └── EmojiPicker.tsx
│ │ │ ├── GithubLoginButton
│ │ │ │ ├── GithubLoginButton.stories.tsx
│ │ │ │ └── GithubLoginButton.tsx
│ │ │ ├── HoverIcon
│ │ │ │ ├── HoverIcon.stories.tsx
│ │ │ │ └── HoverIcon.tsx
│ │ │ ├── InputMessage
│ │ │ │ ├── InputMessage.stories.tsx
│ │ │ │ └── InputMessage.tsx
│ │ │ ├── InputReply
│ │ │ │ ├── InputReply.stories.tsx
│ │ │ │ └── InputReply.tsx
│ │ │ ├── Message
│ │ │ │ ├── Message.stories.tsx
│ │ │ │ └── Message.tsx
│ │ │ ├── MessageReplyBar
│ │ │ │ ├── MessageReplyBar.stories.tsx
│ │ │ │ └── MessageReplyBar.tsx
│ │ │ ├── ProfileModal
│ │ │ │ ├── ProfileModal.stories.tsx
│ │ │ │ └── ProfileModal.tsx
│ │ │ ├── Reply
│ │ │ │ └── Reply.tsx
│ │ │ ├── Section
│ │ │ │ ├── Section.stories.tsx
│ │ │ │ └── Section.tsx
│ │ │ ├── SendMessageButton
│ │ │ │ ├── SendMessageButton.stories.tsx
│ │ │ │ └── SendMessageButton.tsx
│ │ │ ├── UserBox
│ │ │ │ ├── UserBox.stories.tsx
│ │ │ │ └── UserBox.tsx
│ │ │ ├── UserBoxModalSearchBar
│ │ │ │ ├── UserBoxModalSearchBar.stories.tsx
│ │ │ │ └── UserBoxModalSearchBar.tsx
│ │ │ ├── UserBoxModalUserItem
│ │ │ │ ├── UserBoxModalUserItem.stories.tsx
│ │ │ │ └── UserBoxModalUserItem.tsx
│ │ │ ├── WhiteButtonWithIcon
│ │ │ │ ├── WhiteButtonWithIcon.stories.tsx
│ │ │ │ └── WhiteButtonWithIcon.tsx
│ │ │ └── index.ts
│ │ ├── organisms
│ │ │ ├── BrowsePageChannel
│ │ │ │ ├── BrowsePageChannel.stories.tsx
│ │ │ │ └── BrowsePageChannel.tsx
│ │ │ ├── BrowsePageChannelList
│ │ │ │ ├── BrowsePageChannelList.stories.tsx
│ │ │ │ └── BrowsePageChannelList.tsx
│ │ │ ├── BrowsePageHeader
│ │ │ │ ├── BrowsePageHeader.stories.tsx
│ │ │ │ └── BrowsePageHeader.tsx
│ │ │ ├── ChatroomBody
│ │ │ │ ├── ChatroomBody.stories.tsx
│ │ │ │ └── ChatroomBody.tsx
│ │ │ ├── ChatroomHeader
│ │ │ │ ├── ChatroomHeader.stories.tsx
│ │ │ │ └── ChatroomHeader.tsx
│ │ │ ├── CreateChannelModal
│ │ │ │ ├── CreateChannelModal.stories.tsx
│ │ │ │ └── CreateChannelModal.tsx
│ │ │ ├── Header
│ │ │ │ ├── Header.stories.tsx
│ │ │ │ └── Header.tsx
│ │ │ ├── LoginForm
│ │ │ │ ├── LoginForm.stories.tsx
│ │ │ │ └── LoginForm.tsx
│ │ │ ├── Sidebar
│ │ │ │ ├── Sidebar.stories.tsx
│ │ │ │ └── Sidebar.tsx
│ │ │ ├── ThreadBody
│ │ │ │ ├── ThreadBody.tsx
│ │ │ │ └── ThreadReplies.tsx
│ │ │ ├── ThreadHeader
│ │ │ │ └── ThreadHeader.tsx
│ │ │ ├── UserBoxModal
│ │ │ │ ├── UserBoxModal.stories.tsx
│ │ │ │ └── UserBoxModal.tsx
│ │ │ └── index.ts
│ │ └── templates
│ │ │ ├── Body.tsx
│ │ │ ├── FlexContainer.tsx
│ │ │ ├── Main.tsx
│ │ │ ├── MainBox.tsx
│ │ │ └── index.ts
│ ├── index.tsx
│ ├── pages
│ │ ├── ChannelBrowser
│ │ │ └── ChannelBrowser.tsx
│ │ ├── Chatroom
│ │ │ ├── Chatroom.tsx
│ │ │ ├── ChatroomThread.tsx
│ │ │ └── Thread.tsx
│ │ ├── Login.tsx
│ │ ├── LoginLoading.tsx
│ │ └── index.ts
│ └── shared
│ │ └── App.tsx
├── tsconfig.json
└── webpack.config.js
└── server
├── .env.sample
├── .eslintrc.js
├── .prettierrc
├── README.md
├── docker-compose.yml
├── ormconfig.js
├── package-lock.json
├── package.json
├── src
├── application.ts
├── common
│ ├── config
│ │ └── passport.ts
│ ├── constants
│ │ ├── chat-type.ts
│ │ ├── default-section-name.ts
│ │ ├── event-name.ts
│ │ └── http-status-code.ts
│ ├── error
│ │ ├── bad-request-error.ts
│ │ ├── conflict-error.ts
│ │ ├── forbidden-error.ts
│ │ ├── not-found-error.ts
│ │ └── unauthorized-error.ts
│ ├── middleware
│ │ ├── error-handler.ts
│ │ └── redis.ts
│ └── utils
│ │ ├── index.ts
│ │ └── validator.ts
├── controller
│ ├── auth-controller.ts
│ ├── chatroom-controller.ts
│ ├── message-controller.ts
│ ├── message-reaction-controller.ts
│ ├── oauth-controller.ts
│ ├── reaction-controller.ts
│ ├── reply-controller.ts
│ ├── user-chatroom-controller.ts
│ └── user-controller.ts
├── model
│ ├── chatroom.ts
│ ├── message-reaction.ts
│ ├── message.ts
│ ├── reaction.ts
│ ├── reply-reaction.ts
│ ├── reply.ts
│ ├── section.ts
│ ├── socket.ts
│ ├── user-chatroom.ts
│ └── user.ts
├── repository
│ ├── chatroom-repository.ts
│ ├── message-reaction-repository.ts
│ ├── message-repository.ts
│ ├── reacion-repository.ts
│ ├── reply-reaction-repository.ts
│ ├── reply-repository.ts
│ ├── section-repository.ts
│ ├── socket-repository.ts
│ ├── user-chatroom-repository.ts
│ └── user-repository.ts
├── router
│ ├── api.ts
│ ├── auth-router.ts
│ ├── chatroom-router.ts
│ ├── index.ts
│ ├── message-reaction-router.ts
│ ├── message-router.ts
│ ├── oauth.ts
│ ├── reaction-router.ts
│ ├── reply-router.ts
│ ├── user-chatroom-router.ts
│ └── user-router.ts
├── seeds
│ └── chatroom.ts
├── service
│ ├── chatroom-service.ts
│ ├── message-reaction-service.ts
│ ├── message-service.ts
│ ├── reaction-service.ts
│ ├── reply-reaction-service.ts
│ ├── reply-service.ts
│ ├── socket-service.ts
│ ├── user-chatroom-service.ts
│ └── user-service.ts
└── socket
│ ├── event
│ ├── chatroom-event.ts
│ ├── connection-event.ts
│ ├── disconnect-event.ts
│ ├── message-event.ts
│ ├── message-reaction-event.ts
│ ├── reply-event.ts
│ └── reply-rection-event.ts
│ ├── handler
│ ├── chatroom-handler.ts
│ ├── message-handler.ts
│ ├── message-reaction-handler.ts
│ ├── reply-handler.ts
│ ├── reply-reaction-handler.ts
│ └── socket-handler.ts
│ ├── index.ts
│ ├── init-socket.ts
│ ├── middleware
│ └── jwt.ts
│ └── socket.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug request
3 | about: Report a bug
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 |
9 | ---
10 |
11 | ## :warning: 버그 설명
12 |
13 | 버그 설명
14 |
15 | <선택> 이미지 첨부
16 |
17 |
18 |
19 | ## 📑 체크리스트
20 |
21 | > 해결해야하는 버그 체크리스트
22 |
23 | - [ ] 체크 사항 1
24 | - [ ] 체크 사항 2
25 | - [ ] 체크 사항 3
26 | - [ ] 체크 사항 4
27 |
28 |
29 |
30 | ## 🚧 주의 사항
31 |
32 | > 버그를 해결할 때 유의깊게 살펴볼 사항
33 |
34 | - 주의 사항 1
35 | - 주의 사항 2
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 💁 설명
11 |
12 | 이슈 설명
13 |
14 | <선택> 이미지 첨부
15 |
16 |
17 |
18 | ## 📑 체크리스트
19 |
20 | > 구현해야하는 이슈 체크리스트
21 |
22 | - [ ] 체크 사항 1
23 | - [ ] 체크 사항 2
24 | - [ ] 체크 사항 3
25 | - [ ] 체크 사항 4
26 |
27 |
28 |
29 | ## 🚧 주의 사항
30 |
31 | > 이슈를 구현할 때 유의깊게 살펴볼 사항
32 |
33 | - 주의 사항 1
34 | - 주의 사항 2
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## :bookmark_tabs: 제목
2 |
3 | <필수> PR 제목
4 |
5 | <선택> 이미지 첨부
6 |
7 |
8 | ## :speech_balloon: 작업 내용
9 |
10 | > 구현 내용 및 작업 했던 내역
11 |
12 | - [x] 작업 내역 1
13 | - [x] 작업 내역 2
14 | - [x] 작업 내역 3
15 | - [x] 작업 내역 4
16 |
17 |
18 | ## :construction: PR 특이 사항
19 |
20 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점
21 |
22 | - 특이 사항 1
23 | - 특이 사항 2
24 |
25 |
--------------------------------------------------------------------------------
/client/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "corejs": "2",
7 | "useBuiltIns": "entry"
8 | }
9 | ]
10 | ]
11 | }
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es2021": true
6 | },
7 | "ignorePatterns": ["node_modules/"],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "airbnb-base",
12 | "prettier/@typescript-eslint",
13 | "plugin:prettier/recommended"
14 | ],
15 | "globals": {
16 | "Atomics": "readonly",
17 | "SharedArrayBuffer": "readonly"
18 | },
19 | "parser": "@typescript-eslint/parser",
20 | "parserOptions": {
21 | "ecmaFeatures": {
22 | "jsx": true
23 | },
24 | "ecmaVersion": 12,
25 | "sourceType": "module"
26 | },
27 | "plugins": ["react", "@typescript-eslint", "prettier"],
28 | "settings": {
29 | "import/resolver": {
30 | "node": {
31 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
32 | }
33 | }
34 | },
35 | "rules": {
36 | "prettier/prettier": [
37 | "error",
38 | {
39 | "endOfLine": "auto"
40 | }
41 | ],
42 | "linebreak-style": 0,
43 | "no-case-declarations": 0,
44 | "no-use-before-define": "off",
45 | "import/prefer-default-export": "off",
46 | "import/no-unresolved": "off",
47 | "@typescript-eslint/no-use-before-define": ["error"],
48 | "import/extensions": [
49 | "error",
50 | "ignorePackages",
51 | {
52 | "js": "never",
53 | "jsx": "never",
54 | "ts": "never",
55 | "tsx": "never"
56 | }
57 | ],
58 | "no-alert": "off"
59 | }
60 | }
--------------------------------------------------------------------------------
/client/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "trailingComma": "none",
6 | "bracketSpacing": true,
7 | "semi": true,
8 | "useTabs": false,
9 | "arrowParens": "always",
10 | "endOfLine": "lf",
11 | "jsxBracketSameLine": true,
12 | "jsxSingleQuote": false
13 | }
--------------------------------------------------------------------------------
/client/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | "stories": [
5 | "../src/**/*.stories.mdx",
6 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
7 | ],
8 | "addons": [
9 | '@storybook/addon-links',
10 | '@storybook/addon-essentials',
11 | '@storybook/addon-actions',
12 | '@storybook/addon-knobs',
13 | ],
14 | webpackFinal: async(config, { configType }) => {
15 | config.resolve.alias = {
16 | ...config.resolve.alias,
17 | '@components': path.resolve(__dirname, '../src/components'),
18 | '@pages': path.resolve(__dirname, '../src/pages'),
19 | '@theme': path.resolve(__dirname, '../src/common/theme'),
20 | '@utils': path.resolve(__dirname, '../src/common/utils'),
21 | '@store': path.resolve(__dirname, '../src/common/store'),
22 | '@imgs': path.resolve(__dirname, '../public/imgs'),
23 | '@socket': path.resolve(__dirname, '../src/common/socket'),
24 | '@constants': path.resolve(__dirname, '../src/common/constants')
25 | }
26 | return config;
27 | }
28 | }
--------------------------------------------------------------------------------
/client/.storybook/preview.js:
--------------------------------------------------------------------------------
1 |
2 | export const parameters = {
3 | actions: { argTypesRegex: "^on[A-Z].*" },
4 | }
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # How to Start
2 |
3 | ## client
4 |
5 | ### env
6 | > /env/production.env
7 | ```
8 | API_URL=
9 | NODE_ENV=
10 | ```
11 |
12 | ### Start
13 | > Install
14 | ```
15 | npm install
16 | ```
17 | > Start
18 | ```
19 | npm run start
20 | ```
21 |
22 | > Storybook
23 |
24 | ```
25 | npm run storybook
26 | ```
27 |
28 | > Build
29 |
30 | ```
31 | npm run build
32 | ```
33 |
34 |
--------------------------------------------------------------------------------
/client/env/.env.sample:
--------------------------------------------------------------------------------
1 | SERVER_ADDR=localhost:3000
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/imgs/channel-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/channel-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/close-filled-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/close-filled-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/close-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/close-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/detail-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/detail-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/emoji-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/emoji-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/filter-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/filter-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png' {
2 | const value: any;
3 | export = value;
4 | }
5 |
--------------------------------------------------------------------------------
/client/public/imgs/lock-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/lock-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/logo-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/logo-text.png
--------------------------------------------------------------------------------
/client/public/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/logo.png
--------------------------------------------------------------------------------
/client/public/imgs/option-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/option-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/plus-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/plus-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/search-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/send-message-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/send-message-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/sort-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/sort-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/star-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/star-blue.png
--------------------------------------------------------------------------------
/client/public/imgs/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/star.png
--------------------------------------------------------------------------------
/client/public/imgs/thread-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/thread-icon.png
--------------------------------------------------------------------------------
/client/public/imgs/user-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/client/public/imgs/user-icon.png
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Black
8 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/src/common/constants/chat-type.ts:
--------------------------------------------------------------------------------
1 | export const ChatType = {
2 | Message: 'Message',
3 | ReplyTitle: 'ReplyTitle',
4 | Reply: 'Reply'
5 | };
6 |
7 | export type ChatTypes = typeof ChatType[keyof typeof ChatType];
8 |
--------------------------------------------------------------------------------
/client/src/common/constants/chatroom-type.ts:
--------------------------------------------------------------------------------
1 | export const ChatroomType = {
2 | DM: 'DM',
3 | Channel: 'Channel'
4 | };
5 |
6 | export type ChatroomTypes = typeof ChatroomType[keyof typeof ChatroomType];
7 |
--------------------------------------------------------------------------------
/client/src/common/constants/default-section-name.ts:
--------------------------------------------------------------------------------
1 | export const DefaultSectionName = {
2 | CHANNELS: 'Channels',
3 | DIRECT_MESSAGES: 'Direct Messages',
4 | STARRED: 'Starred'
5 | };
6 |
7 | export type DefaultSectionNames = typeof DefaultSectionName[keyof typeof DefaultSectionName];
8 |
--------------------------------------------------------------------------------
/client/src/common/constants/http-status-code.ts:
--------------------------------------------------------------------------------
1 | export const HttpStatusCode = {
2 | OK: 200,
3 | CREATED: 201,
4 | NO_CONTENT: 204,
5 | BAD_REQUEST: 400,
6 | UNAUTHORIZED: 401,
7 | FORBIDDEN: 403,
8 | NOT_FOUND: 404,
9 | CONFLICT: 409,
10 | INTERNAL_SERVER_ERROR: 500
11 | };
12 |
13 | export type HttpStatusCodes = typeof HttpStatusCode[keyof typeof HttpStatusCode];
14 |
--------------------------------------------------------------------------------
/client/src/common/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { ChatType, ChatTypes } from './chat-type';
2 | import { ChatroomType, ChatroomTypes } from './chatroom-type';
3 | import { DefaultSectionName, DefaultSectionNames } from './default-section-name';
4 | import { HttpStatusCode, HttpStatusCodes } from './http-status-code';
5 | import { KeyCode, KeyCodes } from './key-code';
6 | import { ScrollEventType, ScrollEventTypes } from './scroll-event-type';
7 | import { Size, Sizes } from './size';
8 | import { SortMethod, SortMethods } from './sort-method';
9 |
10 | export {
11 | ChatType,
12 | ChatTypes,
13 | ChatroomType,
14 | ChatroomTypes,
15 | DefaultSectionName,
16 | DefaultSectionNames,
17 | HttpStatusCode,
18 | HttpStatusCodes,
19 | KeyCode,
20 | KeyCodes,
21 | ScrollEventType,
22 | ScrollEventTypes,
23 | Size,
24 | Sizes,
25 | SortMethod,
26 | SortMethods
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/common/constants/key-code.ts:
--------------------------------------------------------------------------------
1 | export const KeyCode = {
2 | ENTER: 13
3 | };
4 |
5 | export type KeyCodes = typeof KeyCode[keyof typeof KeyCode];
6 |
--------------------------------------------------------------------------------
/client/src/common/constants/scroll-event-type.ts:
--------------------------------------------------------------------------------
1 | export const ScrollEventType = {
2 | COMMON: 'Common',
3 | LOADING: 'Loading',
4 | COMPLETELOADING: 'Complete loading',
5 | INPUTTEXT: 'Input Text'
6 | };
7 |
8 | export const THROTTLETIME = 50;
9 |
10 | export type ScrollEventTypes = typeof ScrollEventType[keyof typeof ScrollEventType];
11 |
--------------------------------------------------------------------------------
/client/src/common/constants/size.ts:
--------------------------------------------------------------------------------
1 | export const Size = {
2 | BIG: 'big',
3 | LARGE: 'large',
4 | MEDIUM: 'medium',
5 | SMALL: 'small',
6 | SUPER_SMALL: 'superSmall'
7 | };
8 |
9 | export type Sizes = typeof Size[keyof typeof Size];
10 |
--------------------------------------------------------------------------------
/client/src/common/constants/sort-method.ts:
--------------------------------------------------------------------------------
1 | export const SortMethod = {
2 | NEWEST_CHANNEL: 'Newest channel',
3 | OLDEST_CHANNEL: 'Oldest channel',
4 | MOST_MEMBERS: 'Most members',
5 | A_TO_Z: 'A to Z',
6 | Z_TO_A: 'Z to A'
7 | };
8 | export type SortMethods = typeof SortMethod[keyof typeof SortMethod];
9 |
--------------------------------------------------------------------------------
/client/src/common/socket/emits/chatroom.ts:
--------------------------------------------------------------------------------
1 | import { JOIN_CHATROOM, JOIN_DM, LEAVE_CHANNEL } from '@socket/types/chatroom-types';
2 | import socket from '../socketIO';
3 |
4 | export const joinChatroom = (chatroomId: number) => {
5 | socket.emit(JOIN_CHATROOM, { chatroomId });
6 | };
7 |
8 | export const joinDM = (userId: number, chatroomId: number) => {
9 | socket.emit(JOIN_DM, { userId, chatroomId });
10 | };
11 |
12 | export const leaveChannel = (chatroomId: number) => {
13 | socket.emit(LEAVE_CHANNEL, { chatroomId });
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/common/socket/emits/message.ts:
--------------------------------------------------------------------------------
1 | import { CREATE_MESSAGE, createMessageState } from '@socket/types/message-types';
2 | import socket from '../socketIO';
3 |
4 | export const createMessage = (message: createMessageState) => {
5 | socket.emit(CREATE_MESSAGE, message);
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/common/socket/emits/reaction.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CREATE_MESSAGE_REACTION,
3 | DELETE_MESSAGE_REACTION,
4 | CREATE_REPLY_REACTION,
5 | DELETE_REPLY_REACTION,
6 | createMessageReactionState,
7 | deleteMessageReactionState,
8 | createReplyReactionState,
9 | deleteReplyReactionState
10 | } from '@socket/types/reaction-types';
11 | import socket from '../socketIO';
12 |
13 | export const createMessageReaction = (reaction: createMessageReactionState) => {
14 | socket.emit(CREATE_MESSAGE_REACTION, reaction);
15 | };
16 |
17 | export const deleteMessageReaction = (reaction: deleteMessageReactionState) => {
18 | socket.emit(DELETE_MESSAGE_REACTION, reaction);
19 | };
20 |
21 | export const createReplyReaction = (reaction: createReplyReactionState) => {
22 | socket.emit(CREATE_REPLY_REACTION, reaction);
23 | };
24 |
25 | export const deleteReplyReaction = (reaction: deleteReplyReactionState) => {
26 | socket.emit(DELETE_REPLY_REACTION, reaction);
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/common/socket/emits/thread.ts:
--------------------------------------------------------------------------------
1 | import { CREATE_REPLY, createThreadState } from '@socket/types/thread-types';
2 | import socket from '../socketIO';
3 |
4 | export const createReply = (reply: createThreadState) => {
5 | socket.emit(CREATE_REPLY, reply);
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/common/socket/socketIO.ts:
--------------------------------------------------------------------------------
1 | import { io } from 'socket.io-client';
2 |
3 | const socketURL: any = process.env.API_URL;
4 |
5 | const socket = io(socketURL, {
6 | query: { token: window.localStorage.getItem('token') },
7 | transports: ['websocket', 'polling']
8 | });
9 |
10 | export default socket;
11 |
--------------------------------------------------------------------------------
/client/src/common/socket/types/chatroom-types.ts:
--------------------------------------------------------------------------------
1 | export const JOIN_CHATROOM = 'join chatroom';
2 | export const JOIN_DM = 'join DM';
3 | export const LEAVE_CHANNEL = 'leave channel';
4 |
--------------------------------------------------------------------------------
/client/src/common/socket/types/message-types.ts:
--------------------------------------------------------------------------------
1 | export const CREATE_MESSAGE = 'create message';
2 |
3 | export interface createMessageState {
4 | content: string;
5 | chatroomId: number | null;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/common/socket/types/reaction-types.ts:
--------------------------------------------------------------------------------
1 | export const CREATE_MESSAGE_REACTION = 'create reaction';
2 | export const DELETE_MESSAGE_REACTION = 'delete reaction';
3 | export const CREATE_REPLY_REACTION = 'create reply reaction';
4 | export const DELETE_REPLY_REACTION = 'delete reply reaction';
5 |
6 | interface userInfo {
7 | displayName: string;
8 | userId: number;
9 | }
10 |
11 | export interface createMessageReactionState {
12 | messageId: number;
13 | title: string;
14 | emoji: string;
15 | }
16 |
17 | export interface deleteMessageReactionState {
18 | messageId: number;
19 | reactionId: number;
20 | }
21 |
22 | export interface createReplyReactionState {
23 | replyId: number;
24 | title: string;
25 | emoji: string;
26 | }
27 |
28 | export interface deleteReplyReactionState {
29 | replyId: number;
30 | reactionId: number;
31 | }
32 |
33 | export interface socketMessageReactionState {
34 | authors: Array;
35 | chatroomId: number;
36 | emoji: string;
37 | messageId: number;
38 | reactionId: number;
39 | title: string;
40 | }
41 |
42 | export interface socketReplyReactionState {
43 | reactionId: number;
44 | title: string;
45 | emoji: string;
46 | replyId: number;
47 | authors: Array;
48 | messageId: number;
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/common/socket/types/thread-types.ts:
--------------------------------------------------------------------------------
1 | export const CREATE_REPLY = 'create reply';
2 |
3 | export interface createThreadState {
4 | content: string;
5 | messageId: number | null;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/common/store/actions/channel-action.ts:
--------------------------------------------------------------------------------
1 | import {
2 | INIT_CHANNELS_ASYNC,
3 | LOAD_NEXT_CHANNELS_ASYNC,
4 | JOIN_CHANNEL_ASYNC,
5 | LEAVE_CHANNEL,
6 | LoadNextChannelState,
7 | JoinChannelState,
8 | LeaveChannelState
9 | } from '../types/channel-types';
10 |
11 | export const initChannels = () => ({ type: INIT_CHANNELS_ASYNC });
12 | export const loadNextChannels = (payload: LoadNextChannelState) => ({ type: LOAD_NEXT_CHANNELS_ASYNC, payload });
13 | export const joinChannel = (payload: JoinChannelState) => ({ type: JOIN_CHANNEL_ASYNC, payload });
14 | export const leaveChannel = (payload: LeaveChannelState) => ({ type: LEAVE_CHANNEL, payload });
15 |
--------------------------------------------------------------------------------
/client/src/common/store/actions/modal-action.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CREATE_MODAL_OPEN,
3 | CREATE_MODAL_CLOSE,
4 | CHANNEL_MODAL_OPEN,
5 | CHANNEL_MODAL_CLOSE,
6 | USERBOX_MODAL_OPEN,
7 | USERBOX_MODAL_CLOSE,
8 | PROFILE_MODAL_OPEN,
9 | PROFILE_MODAL_CLOSE,
10 | EMOJI_PICKER_OPEN,
11 | EMOJI_PICKER_CLOSE,
12 | ChannelModalState,
13 | ProfileModalState,
14 | EmojiPickerState
15 | } from '@store/types/modal-types';
16 |
17 | export const createModalOpen = () => ({ type: CREATE_MODAL_OPEN });
18 | export const createModalClose = () => ({ type: CREATE_MODAL_CLOSE });
19 | export const channelModalOpen = (payload: ChannelModalState) => ({ type: CHANNEL_MODAL_OPEN, payload });
20 | export const channelModalClose = () => ({ type: CHANNEL_MODAL_CLOSE });
21 | export const userboxModalOpen = () => ({ type: USERBOX_MODAL_OPEN });
22 | export const userboxModalClose = () => ({ type: USERBOX_MODAL_CLOSE });
23 | export const profileModalOpen = (payload: ProfileModalState) => ({ type: PROFILE_MODAL_OPEN, payload });
24 | export const profileModalClose = () => ({ type: PROFILE_MODAL_CLOSE });
25 | export const emojiPickerOpen = (payload: EmojiPickerState) => ({ type: EMOJI_PICKER_OPEN, payload });
26 | export const emojiPickerClose = () => ({ type: EMOJI_PICKER_CLOSE });
27 |
--------------------------------------------------------------------------------
/client/src/common/store/actions/thread-action.ts:
--------------------------------------------------------------------------------
1 | import { socketReplyReactionState } from '@socket/types/reaction-types';
2 | import {
3 | LOAD_THREAD_ASYNC,
4 | INSERT_REPLY,
5 | LOAD_NEXT_REPLIES_ASYNC,
6 | ADD_REPLY_REACTION,
7 | DELETE_REPLY_REACTION,
8 | ReplyState,
9 | AsyncloadNextReplysState,
10 | AsyncLoadThreadState
11 | } from '@store/types/thread-types';
12 |
13 | export const loadThread = (payload: AsyncLoadThreadState) => ({ type: LOAD_THREAD_ASYNC, payload });
14 | export const InsertReply = (payload: ReplyState) => ({ type: INSERT_REPLY, payload });
15 | export const loadNextReplies = (payload: AsyncloadNextReplysState) => ({ type: LOAD_NEXT_REPLIES_ASYNC, payload });
16 | export const createReplyReaction = (payload: socketReplyReactionState) => ({ type: ADD_REPLY_REACTION, payload });
17 | export const deleteReplyReaction = (payload: socketReplyReactionState) => ({ type: DELETE_REPLY_REACTION, payload });
18 |
--------------------------------------------------------------------------------
/client/src/common/store/actions/user-action.ts:
--------------------------------------------------------------------------------
1 | import { LOGIN_ASYNC, LOGOUT } from '../types/user-types';
2 |
3 | export const loginAsync = () => ({ type: LOGIN_ASYNC });
4 | export const logout = () => ({ type: LOGOUT });
5 |
--------------------------------------------------------------------------------
/client/src/common/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 | import { rootReducer } from './reducers';
4 | import { rootSaga } from './sagas';
5 |
6 | const sagaMiddleware = createSagaMiddleware();
7 |
8 | export default createStore(rootReducer, applyMiddleware(sagaMiddleware));
9 |
10 | sagaMiddleware.run(rootSaga);
11 |
--------------------------------------------------------------------------------
/client/src/common/store/reducers/channel-reducer.ts:
--------------------------------------------------------------------------------
1 | import { ChannelState, ChannelsState, ChannelTypes, INIT_CHANNELS, LOAD_NEXT_CHANNELS, JOIN_CHANNEL, LEAVE_CHANNEL } from '../types/channel-types';
2 |
3 | const initialState: ChannelsState = {
4 | channelCount: 0,
5 | channels: []
6 | };
7 |
8 | export default function channelReducer(state = initialState, action: ChannelTypes) {
9 | switch (action.type) {
10 | case INIT_CHANNELS: {
11 | return {
12 | channelCount: action.payload.channelCount,
13 | channels: action.payload.channels
14 | };
15 | }
16 | case LOAD_NEXT_CHANNELS: {
17 | return {
18 | ...state,
19 | channels: [...state.channels, ...action.payload.channels]
20 | };
21 | }
22 | case JOIN_CHANNEL: {
23 | const { chatroomId } = action.payload;
24 | const channels = state.channels.map((channel: ChannelState) => {
25 | if (channel.chatroomId === chatroomId) return { ...channel, isJoined: true, members: channel.members + 1 };
26 | return channel;
27 | });
28 | return { ...state, channels };
29 | }
30 | case LEAVE_CHANNEL: {
31 | const { chatroomId } = action.payload;
32 | const channels = state.channels.map((channel: ChannelState) => {
33 | if (channel.chatroomId === chatroomId) return { ...channel, isJoined: false, members: channel.members - 1 };
34 | return channel;
35 | });
36 |
37 | return { ...state, channels };
38 | }
39 | default: {
40 | return state;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/common/store/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import userReducer from './user-reducer';
3 | import chatroomReducer from './chatroom-reducer';
4 | import modalReducer from './modal-reducer';
5 | import channelReducer from './channel-reducer';
6 | import threadReducer from './thread-reducer';
7 |
8 | export const rootReducer = combineReducers({
9 | user: userReducer,
10 | chatroom: chatroomReducer,
11 | modal: modalReducer,
12 | channel: channelReducer,
13 | thread: threadReducer
14 | });
15 |
16 | export type RootState = ReturnType;
17 |
--------------------------------------------------------------------------------
/client/src/common/store/reducers/user-reducer.ts:
--------------------------------------------------------------------------------
1 | import { UserState, UserTypes } from '@store/types/user-types';
2 |
3 | const initialState: UserState = {
4 | userId: null,
5 | profileUri: '',
6 | displayName: ''
7 | };
8 |
9 | const UserReducer = (state = initialState, action: UserTypes) => {
10 | switch (action.type) {
11 | case 'LOGIN':
12 | return { ...state, ...action.payload };
13 | case 'LOGOUT':
14 | return { ...state, ...initialState };
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default UserReducer;
21 |
--------------------------------------------------------------------------------
/client/src/common/store/sagas/channel-saga.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeEvery } from 'redux-saga/effects';
2 | import API from '@utils/api';
3 | import {
4 | AsyncLoadNextChannels,
5 | AsyncJoinChannel,
6 | INIT_CHANNELS,
7 | INIT_CHANNELS_ASYNC,
8 | JOIN_CHANNEL,
9 | JOIN_CHANNEL_ASYNC,
10 | LOAD_NEXT_CHANNELS,
11 | LOAD_NEXT_CHANNELS_ASYNC
12 | } from '../types/channel-types';
13 | import { ADD_CHANNEL, PICK_CHANNEL_ASYNC } from '../types/chatroom-types';
14 |
15 | function* initChannelsSaga() {
16 | try {
17 | const { channels, channelCount } = yield call(API.getChannels);
18 | yield put({ type: INIT_CHANNELS, payload: { channelCount, channels } });
19 | } catch (e) {
20 | console.log(e);
21 | }
22 | }
23 |
24 | function* loadNextChannels(action: AsyncLoadNextChannels) {
25 | try {
26 | const { title } = action.payload;
27 | const nextChannels = yield call(API.getNextChannels, title);
28 | yield put({ type: LOAD_NEXT_CHANNELS, payload: { channels: nextChannels } });
29 | } catch (e) {
30 | console.log(e);
31 | }
32 | }
33 |
34 | function* joinChannel(action: AsyncJoinChannel) {
35 | try {
36 | const { chatroomId } = action.payload;
37 | yield call(API.joinChannel, chatroomId);
38 | yield put({ type: JOIN_CHANNEL, payload: { chatroomId } });
39 | const chatroom = yield call(API.getChatroom, chatroomId);
40 | const { chatType, isPrivate, title } = chatroom;
41 | const payload = { chatroomId, chatType, isPrivate, title };
42 | yield put({ type: ADD_CHANNEL, payload });
43 | yield put({ type: PICK_CHANNEL_ASYNC, payload: { selectedChatroomId: chatroomId } });
44 | } catch (e) {
45 | console.log(e);
46 | }
47 | }
48 |
49 | export function* channelSaga() {
50 | yield takeEvery(INIT_CHANNELS_ASYNC, initChannelsSaga);
51 | yield takeEvery(LOAD_NEXT_CHANNELS_ASYNC, loadNextChannels);
52 | yield takeEvery(JOIN_CHANNEL_ASYNC, joinChannel);
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/common/store/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 | import { chatroomSaga } from './chatroom-saga';
3 | import { userSaga } from './user-saga';
4 | import { channelSaga } from './channel-saga';
5 | import { threadSaga } from './thread-saga';
6 |
7 | export function* rootSaga() {
8 | yield all([chatroomSaga(), userSaga(), channelSaga(), threadSaga()]);
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/common/store/sagas/thread-saga.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeEvery } from 'redux-saga/effects';
2 | import API from '@utils/api';
3 | import {
4 | LOAD_THREAD,
5 | LOAD_THREAD_ASYNC,
6 | LOAD_NEXT_REPLIES,
7 | LOAD_NEXT_REPLIES_ASYNC,
8 | AsyncLoadThread,
9 | AsyncLoadNextThreadReplys
10 | } from '@store/types/thread-types';
11 |
12 | function* loadThreadSaga(action: AsyncLoadThread) {
13 | try {
14 | const { messageId } = action.payload;
15 | const payload = yield call(API.getThread, messageId);
16 | const { title } = yield call(API.getChatroom, payload.message.chatroom.chatroomId);
17 | yield put({ type: LOAD_THREAD, payload: { ...payload, title } });
18 | } catch (e) {
19 | console.log(e);
20 | }
21 | }
22 |
23 | function* loadNextReplies(action: AsyncLoadNextThreadReplys) {
24 | try {
25 | const { offsetReply, messageId } = action.payload;
26 | const offsetId = offsetReply.replyId;
27 | const { replies } = yield call(API.getNextReplies, messageId, offsetId);
28 | yield put({ type: LOAD_NEXT_REPLIES, payload: { replies } });
29 | } catch (e) {
30 | console.log(e);
31 | }
32 | }
33 |
34 | export function* threadSaga() {
35 | yield takeEvery(LOAD_THREAD_ASYNC, loadThreadSaga);
36 | yield takeEvery(LOAD_NEXT_REPLIES_ASYNC, loadNextReplies);
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/common/store/sagas/user-saga.ts:
--------------------------------------------------------------------------------
1 | import { call, put, takeEvery } from 'redux-saga/effects';
2 | import API from '@utils/api';
3 | import { LOGIN, LOGIN_ASYNC } from '../types/user-types';
4 |
5 | function* userLoginSaga() {
6 | try {
7 | const payload = yield call(API.getUserInfo);
8 | yield put({ type: LOGIN, payload });
9 | } catch (e) {
10 | console.log(e);
11 | }
12 | }
13 |
14 | export function* userSaga() {
15 | yield takeEvery(LOGIN_ASYNC, userLoginSaga);
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/common/store/types/channel-types.ts:
--------------------------------------------------------------------------------
1 | export const INIT_CHANNELS = 'INIT_CHANNELS';
2 | export const INIT_CHANNELS_ASYNC = 'INIT_CHANNELS_ASYNC';
3 | export const LOAD_NEXT_CHANNELS = 'LOAD_NEXT_CHANNELS';
4 | export const LOAD_NEXT_CHANNELS_ASYNC = 'LOAD_NEXT_CHANNELS_ASYNC';
5 | export const JOIN_CHANNEL = 'JOIN_CHANNEL';
6 | export const JOIN_CHANNEL_ASYNC = 'JOIN_CHANNEL_ASYNC';
7 | export const LEAVE_CHANNEL = 'LEAVE_CHANNEL';
8 |
9 | export interface ChannelState {
10 | chatroomId: number;
11 | title: string;
12 | description?: string;
13 | isPrivate: boolean;
14 | members: number;
15 | isJoined: boolean;
16 | }
17 |
18 | export interface ChannelsState {
19 | channelCount: number;
20 | channels: Array;
21 | }
22 |
23 | export interface JoinChannelState {
24 | chatroomId: number;
25 | }
26 |
27 | export interface LeaveChannelState {
28 | chatroomId: number;
29 | }
30 |
31 | export interface LoadNextChannelState {
32 | title: string;
33 | }
34 |
35 | export interface AsyncLoadNextChannels {
36 | type: typeof LOAD_NEXT_CHANNELS_ASYNC;
37 | payload: LoadNextChannelState;
38 | }
39 |
40 | export interface AsyncJoinChannel {
41 | type: typeof JOIN_CHANNEL_ASYNC;
42 | payload: JoinChannelState;
43 | }
44 |
45 | interface InitChannelsAction {
46 | type: typeof INIT_CHANNELS;
47 | payload: ChannelsState;
48 | }
49 |
50 | interface LoadNextChannelsAction {
51 | type: typeof LOAD_NEXT_CHANNELS;
52 | payload: ChannelsState;
53 | }
54 |
55 | interface JoinChannelAction {
56 | type: typeof JOIN_CHANNEL;
57 | payload: JoinChannelState;
58 | }
59 |
60 | interface LeaveChannelAction {
61 | type: typeof LEAVE_CHANNEL;
62 | payload: LeaveChannelState;
63 | }
64 |
65 | export type ChannelTypes = InitChannelsAction | LoadNextChannelsAction | JoinChannelAction | LeaveChannelAction;
66 |
--------------------------------------------------------------------------------
/client/src/common/store/types/message-types.ts:
--------------------------------------------------------------------------------
1 | import { UserState } from './user-types';
2 | import { ReactionsState } from './reactions-type';
3 | import { ChatroomThreadState } from './chatroom-types';
4 |
5 | export interface MessageState {
6 | messageId: number;
7 | createdAt: Date;
8 | updatedAt: Date;
9 | user: UserState;
10 | messageReactions: Array;
11 | thread: ChatroomThreadState;
12 | }
13 |
14 | export interface MessagesState {
15 | messages: Array;
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/common/store/types/reactions-type.ts:
--------------------------------------------------------------------------------
1 | export interface ReactionsState {
2 | reactionId: number;
3 | title: string;
4 | reactionCount: number;
5 | reactionDisplayNames: Array;
6 | emoji: string;
7 | }
8 |
9 | export interface ReplyReactionsState {
10 | reactionId: number;
11 | title: string;
12 | reactionCount: number;
13 | replyDisplayNames: Array;
14 | emoji: string;
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/common/store/types/user-types.ts:
--------------------------------------------------------------------------------
1 | export const LOGIN = 'LOGIN';
2 | export const LOGIN_ASYNC = 'LOGIN_ASYNC';
3 | export const LOGOUT = 'LOGOUT';
4 |
5 | export interface UserState {
6 | userId: number | null;
7 | profileUri: string;
8 | displayName: string;
9 | }
10 |
11 | interface UserLoginAction {
12 | type: typeof LOGIN;
13 | payload: UserState;
14 | }
15 |
16 | interface UserLogoutAction {
17 | type: typeof LOGOUT;
18 | }
19 |
20 | export type UserTypes = UserLoginAction | UserLogoutAction;
21 |
--------------------------------------------------------------------------------
/client/src/common/theme/color.ts:
--------------------------------------------------------------------------------
1 | const color = {
2 | primary: 'black',
3 | secondary: '#1a1d21',
4 | tertiary: 'white',
5 | quaternary: 'rgb(246, 246, 246)',
6 | text_primary: 'rgb(198, 199, 200)',
7 | text_secondary: 'white',
8 | text_tertiary: 'rgba(147,147,147,1)',
9 | text_quaternary: 'rgba(83,83,83,1)',
10 | text_quinary: 'rgb(160, 158, 169)',
11 | text_senary: 'rgb(98, 100, 136)',
12 | text_septenary: '#186b8e',
13 | border_primary: '#e2e2e2',
14 | border_secondary: '#c6c6c6',
15 | border_tertiary: '#1d9bd1',
16 | light_primary: '#33e600',
17 | box_shadow_primary: 'rgba(27, 31, 35, 0.075)',
18 | box_shadow_secondary: 'rgba(3, 102, 214, 0.3)',
19 | box_shadow_tertiary: 'rgba(255, 255, 255, 0.1)',
20 | modal_bg_outer_primary: 'rgba(0, 0, 0, 0.5)',
21 | modal_bg_inner_primary: 'white',
22 | modal_bg_inner_secondary: 'rgba(248, 248, 248, 1)',
23 | selected_chatroom: '#0576b9',
24 | hover_primary: 'rgb(248, 248, 248)',
25 | hover_secondary: 'rgb(18, 100, 163)',
26 | button_secondary: '#017a5a',
27 | button_tertiary: 'rgba(221,221,221,1)',
28 | sidebar_bg: '#1a1e22',
29 | sidebar_border: '#313537',
30 | emoji_bg: 'rgb(232, 245, 250)'
31 | };
32 |
33 | export default color;
34 |
--------------------------------------------------------------------------------
/client/src/common/theme/index.ts:
--------------------------------------------------------------------------------
1 | import color from './color';
2 |
3 | export { color };
4 |
--------------------------------------------------------------------------------
/client/src/common/utils/auth.ts:
--------------------------------------------------------------------------------
1 | export const checkAuthToToken = () => {
2 | return !!localStorage.getItem('token');
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/common/utils/blockPage.ts:
--------------------------------------------------------------------------------
1 | import { auth } from '@utils/index';
2 |
3 | const blockPage = () => {
4 | if (auth.checkAuthToToken() && window.location.pathname === '/login') {
5 | window.location.href = '/';
6 | }
7 | if (!auth.checkAuthToToken() && window.location.pathname !== '/login') {
8 | window.location.href = '/login';
9 | }
10 | };
11 |
12 | export { blockPage };
13 |
--------------------------------------------------------------------------------
/client/src/common/utils/index.ts:
--------------------------------------------------------------------------------
1 | import API from './api';
2 | import * as uriParser from './uriParser';
3 | import { blockPage } from './blockPage';
4 | import registerToken from './registerToken';
5 | import { logout } from './logout';
6 | import * as auth from './auth';
7 |
8 | export { API, uriParser, blockPage, registerToken, logout, auth };
9 |
--------------------------------------------------------------------------------
/client/src/common/utils/logout.ts:
--------------------------------------------------------------------------------
1 | export const logout = () => {
2 | window.localStorage.removeItem('token');
3 | window.location.href = '/login';
4 | };
5 |
--------------------------------------------------------------------------------
/client/src/common/utils/modal.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { profileModalOpen } from '@store/actions/modal-action';
3 |
4 | interface User {
5 | userId: number;
6 | profileUri: string;
7 | displayName: string;
8 | }
9 |
10 | export const openProfileModal = (user: User) => {
11 | const dispatch = useDispatch();
12 | return (e: any) => {
13 | const x = window.pageXOffset + e.target.getBoundingClientRect().left;
14 | const y = window.pageYOffset + e.target.getBoundingClientRect().top;
15 | const { userId, profileUri, displayName } = user;
16 | dispatch(profileModalOpen({ x, y, userId, profileUri, displayName }));
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/common/utils/registerToken.ts:
--------------------------------------------------------------------------------
1 | import { API, uriParser, auth } from '@utils/index';
2 |
3 | const registerToken = async () => {
4 | if (uriParser.isExistParseCodeUrl()) {
5 | const code = uriParser.getCode();
6 | const token = await API.getToken(code);
7 | if (token) {
8 | localStorage.setItem('token', token);
9 | window.location.href = '/client/1';
10 | }
11 | } else if (auth.checkAuthToToken()) window.location.href = '/client/1';
12 | else window.location.href = '/login';
13 | };
14 |
15 | export default registerToken;
16 |
--------------------------------------------------------------------------------
/client/src/common/utils/scroll.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | export const moveScrollToTheBottom = (El: any) => {
3 | const { scrollHeight, clientHeight } = El.current;
4 | const maxScrollTop = scrollHeight - clientHeight;
5 | El.current.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/common/utils/time.ts:
--------------------------------------------------------------------------------
1 | const untisOfTime = {
2 | seconds: 1,
3 | minutes: 60,
4 | hours: 60 * 60,
5 | days: 24 * 60 * 60,
6 | months: ((365 + 365 + 365 + 365 + 366) / 5 / 12) * 24 * 60 * 60,
7 | years: ((365 + 365 + 365 + 365 + 366) / 5) * 24 * 60 * 60
8 | };
9 |
10 | const timeAgo = (date: Date): string => {
11 | const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
12 |
13 | const minutes = timeDiff / untisOfTime.minutes;
14 | const hours = timeDiff / untisOfTime.hours;
15 | const days = timeDiff / untisOfTime.days;
16 | const months = timeDiff / untisOfTime.months;
17 | const years = timeDiff / untisOfTime.years;
18 |
19 | if (minutes < 1) return `1 minute ago`;
20 | if (minutes < 60) return `${Math.round(minutes)} minute ago`;
21 | if (hours < 24) return `${Math.round(hours)} hours ago`;
22 | if (days < 31) return `${Math.round(days)} days ago`;
23 | if (months < 12) return `${Math.round(months)} months ago`;
24 | return `${Math.round(years)} years ago`;
25 | };
26 |
27 | const getTimeConversionValue = (date: Date): string => {
28 | const time = new Date(date);
29 | return time.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
30 | };
31 |
32 | export { timeAgo, getTimeConversionValue };
33 |
--------------------------------------------------------------------------------
/client/src/common/utils/uriParser.ts:
--------------------------------------------------------------------------------
1 | export const isExistParseCodeUrl = () => {
2 | const pattern = new RegExp(/^\?code=\w+$/);
3 | return pattern.test(window.location.search);
4 | };
5 |
6 | export const getCode = () => {
7 | const pattern = new RegExp(/[^(?code=)]\w+/);
8 | const code = pattern.exec(window.location.search);
9 | return code ? code[0] : null;
10 | };
11 |
12 | export const getChatroomId = () => {
13 | const chatroomUrlpattern = new RegExp(/^\/client\/[0-9]+(\/thread\/[0-9]+)*$/);
14 | if (chatroomUrlpattern.test(window.location.pathname)) {
15 | const pattern = new RegExp(/[0-9]+/g);
16 | const code = pattern.exec(window.location.pathname);
17 | return code ? Number(code[0]) : null;
18 | }
19 | return null;
20 | };
21 |
22 | export const getThreadId = () => {
23 | const threadUrlpattern = new RegExp(/^\/client\/[0-9]+(\/thread\/[0-9]+)$/);
24 | if (threadUrlpattern.test(window.location.pathname)) {
25 | const pattern = new RegExp(/[0-9]+$/g);
26 | const code = pattern.exec(window.location.pathname);
27 | return code ? Number(code[0]) : null;
28 | }
29 | return null;
30 | };
31 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ActiveLight/ActiveLight.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Size } from '@constants/index';
4 | import { ActiveLight, ActiveLightProps } from './ActiveLight';
5 |
6 | export default {
7 | title: 'atom/ActiveLight',
8 | component: ActiveLight
9 | } as Meta;
10 |
11 | const Template: Story = (args) => ;
12 |
13 | export const LargeActiveLight = Template.bind({});
14 | LargeActiveLight.args = {
15 | size: Size.LARGE
16 | };
17 |
18 | export const MediumActiveLight = Template.bind({});
19 | MediumActiveLight.args = {
20 | size: Size.MEDIUM
21 | };
22 |
23 | export const SmallActiveLight = Template.bind({});
24 | SmallActiveLight.args = {
25 | size: Size.SMALL
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ActiveLight/ActiveLight.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 | import { Size, Sizes } from '@constants/index';
5 |
6 | interface ActiveLightProps {
7 | size?: Sizes;
8 | isActive?: boolean;
9 | }
10 |
11 | const ActiveLightContainter = styled.div`
12 | width: ${({ size }) => {
13 | if (size === Size.LARGE) return '0.5rem';
14 | if (size === Size.MEDIUM) return '0.4rem';
15 | return '0.3rem';
16 | }};
17 | height: ${({ size }) => {
18 | if (size === Size.LARGE) return '0.5rem';
19 | if (size === Size.MEDIUM) return '0.4rem';
20 | return '0.3rem';
21 | }};
22 | border-width: ${({ size }) => {
23 | if (size === Size.LARGE) return '0.17rem';
24 | if (size === Size.MEDIUM) return '0.13rem';
25 | return '0.1rem';
26 | }};
27 | border-color: ${({ isActive }) => (isActive ? color.light_primary : color.border_secondary)};
28 | border-style: solid;
29 | border-radius: 1rem;
30 | background-color: ${({ isActive }) => (isActive ? color.light_primary : color.primary)};
31 | `;
32 |
33 | const ActiveLight: React.FC = ({ size = Size.MEDIUM, isActive = true, ...props }) => {
34 | return ;
35 | };
36 |
37 | export { ActiveLight, ActiveLightProps };
38 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Button, ButtonProps } from './Button';
4 |
5 | export default {
6 | title: 'atom/Button',
7 | component: Button,
8 | argTypes: {
9 | backgroundColor: { control: 'color' },
10 | borderColor: { control: 'color' },
11 | fontColor: { control: 'color' }
12 | }
13 | } as Meta;
14 |
15 | const Template: Story = (args) => ;
16 |
17 | export const ExampleButton = Template.bind({});
18 | ExampleButton.args = {
19 | children: 'Example'
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 |
5 | interface ButtonProps {
6 | children: React.ReactNode;
7 | backgroundColor: string;
8 | borderColor: string;
9 | fontColor: string;
10 | isBold?: boolean;
11 | hoverColor?: string;
12 | width?: string;
13 | height?: string;
14 | onClick?: () => void;
15 | }
16 |
17 | const StyledButton = styled.button`
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | background-color: ${(props) => props.backgroundColor};
22 | border: 2px solid ${(props) => props.borderColor};
23 | color: ${(props) => props.fontColor};
24 | padding: 0.3rem 1rem;
25 | border-radius: 0.4rem;
26 | outline: none;
27 | cursor: pointer;
28 | font-weight: ${(props) => (props.isBold ? 'bold' : null)};
29 | ${(props) => (props.hoverColor ? `&:hover { background-color: ${color.hover_primary}}` : '')}
30 | ${(props) => (props.width ? `width: ${props.width}}` : '')}
31 | ${(props) => (props.height ? `height: ${props.height}}` : '')}
32 | `;
33 |
34 | const Button: React.FC = ({ children, backgroundColor, borderColor, fontColor, isBold, hoverColor, ...props }) => {
35 | return (
36 |
43 | {children}
44 |
45 | );
46 | };
47 |
48 | export { Button, ButtonProps };
49 |
--------------------------------------------------------------------------------
/client/src/components/atoms/DropMenuBox/DropMenuBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { DropMenuBox, DropMenuBoxProps } from './DropMenuBox';
4 |
5 | export default {
6 | title: 'atom/DropMenuBox',
7 | component: DropMenuBox
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const PrimaryDropMenuBox = Template.bind({});
13 | PrimaryDropMenuBox.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/atoms/DropMenuBox/DropMenuBox.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { color } from '@theme/index';
3 | import styled from 'styled-components';
4 |
5 | interface DropMenuBoxProps {
6 | children: React.ReactNode;
7 | x?: number;
8 | y?: number;
9 | onClick?: () => void;
10 | }
11 |
12 | const BackgroundModal = styled.div`
13 | position: fixed;
14 | top: 0;
15 | left: 0;
16 | display: flex;
17 | width: 100vw;
18 | height: 100vh;
19 | background-color: none;
20 | z-index: 998;
21 | `;
22 |
23 | const InnerModal = styled.div`
24 | position: absolute;
25 | top: ${(props) => `${props.y}px`};
26 | left: ${(props) => `${props.x}px`};
27 | background-color: ${color.modal_bg_inner_secondary};
28 | z-index: 999;
29 | border-radius: 6px;
30 | box-shadow: 0 0 0 1px rgb(248 248 248), 0 4px 12px 0 rgba(0, 0, 0, 0.12);
31 | height: fit-content;
32 | `;
33 |
34 | const DropMenuBox: React.FC = ({ children, x = 0, y = 0, onClick, ...props }) => {
35 | const innerModalRef = useRef();
36 | const offset = -10;
37 | const [nx, setNx] = useState(x);
38 | const [ny, setNy] = useState(y);
39 | useEffect(() => {
40 | const clientWidth = Number(innerModalRef.current?.clientWidth);
41 | const clientHeight = Number(innerModalRef.current?.clientHeight);
42 | const { innerWidth, innerHeight } = window;
43 | if (x + clientWidth >= innerWidth) setNx(innerWidth - clientWidth + offset);
44 | if (y + clientHeight >= innerHeight) setNy(innerHeight - clientHeight + offset);
45 | }, [x, y]);
46 | return (
47 | <>
48 |
49 |
50 | {children}
51 |
52 | >
53 | );
54 | };
55 |
56 | export { DropMenuBox, DropMenuBoxProps };
57 |
--------------------------------------------------------------------------------
/client/src/components/atoms/DropMenuItem/DropMenuItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { DropMenuItem, DropMenuItemProps } from './DropMenuItem';
4 |
5 | export default {
6 | title: 'atom/DropMenuItem',
7 | component: DropMenuItem
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const PrimaryDropMenuItem = Template.bind({});
13 | PrimaryDropMenuItem.args = {
14 | children: 'item'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/atoms/DropMenuItem/DropMenuItem.tsx:
--------------------------------------------------------------------------------
1 | import { color } from '@theme/index';
2 | import React from 'react';
3 | import styled from 'styled-components';
4 |
5 | interface DropMenuItemProps {
6 | children: React.ReactNode;
7 | onClick?: () => void;
8 | }
9 |
10 | const StyledDropMenuItem = styled.div`
11 | width: 100%;
12 | font-weight: 500;
13 | padding: 0.2rem 0rem;
14 | padding-left: 1.5rem;
15 | width: 13rem;
16 | color: ${color.text_tertiary};
17 | cursor: pointer;
18 | &:hover {
19 | color: ${color.text_secondary};
20 | background-color: ${color.hover_secondary};
21 | }
22 | `;
23 |
24 | const DropMenuItem: React.FC = ({ children, onClick }) => {
25 | return {children};
26 | };
27 |
28 | export { DropMenuItem, DropMenuItemProps };
29 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Emoji/Emoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Emoji, EmojiProps } from './Emoji';
4 |
5 | export default {
6 | title: 'atom/Emoji',
7 | component: Emoji
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const GoodEmoji = Template.bind({});
13 | GoodEmoji.args = {
14 | text: '👍'
15 | };
16 |
17 | export const HeartEmoji = Template.bind({});
18 | HeartEmoji.args = {
19 | text: '❤'
20 | };
21 |
22 | export const CheckEmoji = Template.bind({});
23 | CheckEmoji.args = {
24 | text: '✔'
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Emoji/Emoji.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | interface EmojiProps {
5 | text: string;
6 | }
7 |
8 | const StyledEmoji = styled.p`
9 | margin: 0;
10 | `;
11 |
12 | const Emoji: React.FC = ({ text }) => {
13 | return {text};
14 | };
15 |
16 | export { Emoji, EmojiProps };
17 |
--------------------------------------------------------------------------------
/client/src/components/atoms/HoverInput/HoverInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { HoverInput, HoverInputProps } from './HoverInput';
4 |
5 | export default {
6 | title: 'atom/HoverInput',
7 | component: HoverInput
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackHoverInput = Template.bind({});
13 | BlackHoverInput.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/atoms/HoverInput/HoverInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 |
5 | interface HoverInputProps {
6 | placeholder?: string;
7 | onChange?: (e: any) => void;
8 | }
9 |
10 | const StyledHoverInput = styled.input`
11 | padding: 0rem 2%;
12 | width: 96%;
13 | height: 2.5rem;
14 | font-size: 1rem;
15 | outline: none;
16 | border: 1px solid ${color.primary};
17 | border-radius: 0.3rem;
18 |
19 | &:focus {
20 | box-shadow: inset 0 1px 2px ${color.box_shadow_primary}, 0 0 0 3px ${color.box_shadow_secondary};
21 | containerStyle.border = 1px solid ${color.primary};
22 | }
23 | `;
24 |
25 | const HoverInput: React.FC = ({ placeholder, onChange, ...props }) => {
26 | return ;
27 | };
28 |
29 | export { HoverInput, HoverInputProps };
30 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Size, Sizes } from '@constants/index';
4 |
5 | interface IconProps {
6 | size?: Sizes;
7 | isHover?: boolean;
8 | src?: string;
9 | isSelect?: boolean;
10 | }
11 |
12 | const IconContainter = styled.div`
13 | width: ${({ size }) => {
14 | if (size === Size.LARGE) return '1.5rem';
15 | if (size === Size.MEDIUM) return '1.3rem';
16 | return '0.8rem';
17 | }};
18 | height: ${({ size }) => {
19 | if (size === Size.LARGE) return '1.5rem';
20 | if (size === Size.MEDIUM) return '1.3rem';
21 | return '0.8rem';
22 | }};
23 | `;
24 |
25 | const Img = styled.img`
26 | width: inherit;
27 | height: inherit;
28 | ${({ isHover }) => (isHover ? '&:hover { opacity: .5; };' : '')}
29 | ${({ isSelect }) => (isSelect ? 'filter: brightness(1.25);' : '')}
30 | `;
31 |
32 | const Icon: React.FC = ({ size = Size.MEDIUM, isSelect = false, isHover = true, src = '', ...props }) => {
33 | return (
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export { Icon, IconProps };
41 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Input, InputProps } from './Input';
4 |
5 | export default {
6 | title: 'atom/Input',
7 | component: Input
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackInput = Template.bind({});
13 | BlackInput.args = {
14 | id: 'thread',
15 | title: '5주-그룹-프로젝트-슬랙b'
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { KeyCode } from '@constants/index';
4 |
5 | interface InputProps {
6 | title?: string;
7 | id: string;
8 | isThread?: boolean;
9 | content: string;
10 | setContent: any;
11 | keyPressEnter: any;
12 | }
13 |
14 | const StyledInput = styled.input`
15 | width: 100%;
16 | font-size: 1rem;
17 | border: none;
18 | outline: none;
19 | :placeholder-shown {
20 | text-overflow: ellipsis;
21 | }
22 | `;
23 |
24 | const Label = styled.label`
25 | position: absolute;
26 | font-size: 0;
27 | color: white;
28 | `;
29 |
30 | const Input: React.FC = ({ id, title, isThread = false, content, setContent, keyPressEnter, ...props }) => {
31 | const handlingKeyPressEnter = (e: any) => {
32 | if (e.charCode === KeyCode.ENTER) keyPressEnter(e.target.value);
33 | };
34 | const handlingChange = (e: any) => {
35 | setContent(e.target.value);
36 | };
37 | return (
38 | <>
39 |
47 |
48 | >
49 | );
50 | };
51 |
52 | export { Input, InputProps };
53 |
--------------------------------------------------------------------------------
/client/src/components/atoms/LogoImg/LogoImg.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { LogoImg, LogoImgProps } from './LogoImg';
4 |
5 | export default {
6 | title: 'atom/LogoImg',
7 | component: LogoImg
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackLogoImg = Template.bind({});
13 | BlackLogoImg.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/atoms/LogoImg/LogoImg.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import LogoText from '@imgs/logo-text.png';
4 | import { Size, Sizes } from '@constants/index';
5 |
6 | interface LogoImgProps {
7 | size?: Sizes;
8 | }
9 |
10 | const StyledLogoImg = styled.img`
11 | height: ${({ size }) => (size === Size.SMALL ? '2.4rem' : '5rem')};
12 | margin-bottom: -0.2rem;
13 | cursor: pointer;
14 | `;
15 |
16 | const LogoImg: React.FC = ({ size = Size.SMALL, ...props }) => {
17 | return ;
18 | };
19 |
20 | export { LogoImg, LogoImgProps };
21 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ModalBox/ModalBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ModalBox, ModalBoxProps } from './ModalBox';
4 |
5 | export default {
6 | title: 'atom/ModalBox',
7 | component: ModalBox
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const PrimaryModalBox = Template.bind({});
13 | PrimaryModalBox.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ModalBox/ModalBox.tsx:
--------------------------------------------------------------------------------
1 | import { color } from '@theme/index';
2 | import React, { useRef } from 'react';
3 | import styled from 'styled-components';
4 |
5 | interface ModalBoxProps {
6 | children: React.ReactNode;
7 | onClick: () => void;
8 | }
9 |
10 | const BackgroundModal = styled.div`
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | width: 100vw;
18 | height: 100vh;
19 | background-color: ${color.modal_bg_outer_primary};
20 | z-index: 998;
21 | `;
22 |
23 | const InnerModal = styled.div`
24 | padding: 2rem;
25 | background-color: ${color.modal_bg_inner_primary};
26 | z-index: 999;
27 | border-radius: 1rem;
28 | `;
29 |
30 | const ModalBox: React.FC = ({ children, onClick, ...props }) => {
31 | const BackgroundModalRef = useRef();
32 |
33 | const handlingBackgroundModalClick = (e: any) => {
34 | if (e.target === BackgroundModalRef.current) {
35 | onClick();
36 | }
37 | };
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
46 | export { ModalBox, ModalBoxProps };
47 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileImg/ProfileImg.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Size } from '@constants/index';
4 | import { ProfileImg, ProfileImgProps } from './ProfileImg';
5 |
6 | export default {
7 | title: 'atom/ProfileImg',
8 | component: ProfileImg
9 | } as Meta;
10 |
11 | const Template: Story = (args) => ;
12 |
13 | export const LargeProfileImg = Template.bind({});
14 | LargeProfileImg.args = {
15 | size: Size.LARGE
16 | };
17 |
18 | export const MediumProfileImg = Template.bind({});
19 | MediumProfileImg.args = {
20 | size: Size.MEDIUM
21 | };
22 |
23 | export const SmallProfileImg = Template.bind({});
24 | SmallProfileImg.args = {
25 | size: Size.SMALL
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileImg/ProfileImg.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Logo from '@imgs/logo.png';
4 | import { color } from '@theme/index';
5 | import { Size, Sizes } from '@constants/index';
6 |
7 | interface ProfileImgProps {
8 | size?: Sizes;
9 | isHover?: boolean;
10 | src?: string;
11 | }
12 |
13 | const ProfileImgContainter = styled.div`
14 | display: absolute;
15 | width: ${({ size }) => {
16 | if (size === Size.LARGE) return '2.2rem';
17 | if (size === Size.MEDIUM) return '1.4rem';
18 | return '0.7rem';
19 | }};
20 | height: ${({ size }) => {
21 | if (size === Size.LARGE) return '2.2rem';
22 | if (size === Size.MEDIUM) return '1.4rem';
23 | return '0.7rem';
24 | }};
25 | border-radius: ${({ size }) => {
26 | if (size === Size.LARGE) return '0.5rem';
27 | if (size === Size.MEDIUM) return '0.3rem';
28 | return '0.2rem';
29 | }};
30 | background-color: ${color.primary};
31 | `;
32 |
33 | const Img = styled.img`
34 | width: 100%;
35 | height: 100%;
36 | border-radius: ${({ size }) => {
37 | if (size === Size.LARGE) return '0.5rem';
38 | if (size === Size.MEDIUM) return '0.3rem';
39 | return '0.2rem';
40 | }};
41 | ${({ isHover }) => (isHover ? '&:hover { opacity: .5; };' : '')}
42 | `;
43 |
44 | const ProfileImg: React.FC = ({ size = Size.MEDIUM, isHover = false, src = Logo, ...props }) => {
45 | return (
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export { ProfileImg, ProfileImgProps };
53 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileModalBody/ProfileModalBody.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ProfileModalBody, ProfileModalBodyProps } from './ProfileModalBody';
4 |
5 | export default {
6 | title: 'atom/ProfileModalBody',
7 | component: ProfileModalBody
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackProfileModalBody = Template.bind({});
13 | BlackProfileModalBody.args = {
14 | displayName: 'profornnan'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileModalBody/ProfileModalBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 | import { ActiveLight, Text } from '@components/atoms';
5 | import { getTimeConversionValue } from '@utils/time';
6 | import { Size } from '@constants/index';
7 |
8 | interface ProfileModalBodyProps {
9 | displayName: string;
10 | }
11 |
12 | const ProfileModalBodyContainter = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 | padding: 1rem;
16 | `;
17 |
18 | const DisplayNameWrap = styled.div`
19 | display: flex;
20 | align-items: center;
21 | p {
22 | margin-right: 0.5rem;
23 | }
24 | `;
25 |
26 | const LocalTimeWrap = styled.div`
27 | margin-top: 1rem;
28 | `;
29 |
30 | const ProfileModalBody: React.FC = ({ displayName, ...props }) => {
31 | const [localTime, setLocalTime] = useState(getTimeConversionValue(new Date()));
32 |
33 | useEffect(() => {
34 | const timerId = setInterval(() => {
35 | setLocalTime(getTimeConversionValue(new Date()));
36 | }, 1000);
37 | return () => {
38 | clearInterval(timerId);
39 | };
40 | }, [localTime]);
41 |
42 | return (
43 |
44 |
45 |
46 | {displayName}
47 |
48 |
49 |
50 |
51 |
52 | Local time
53 |
54 |
55 | {localTime}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export { ProfileModalBody, ProfileModalBodyProps };
63 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileModalImg/ProfileModalImg.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ProfileModalImg, ProfileModalImgProps } from './ProfileModalImg';
4 |
5 | export default {
6 | title: 'atom/ProfileModalImg',
7 | component: ProfileModalImg
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackProfileModalImg = Template.bind({});
13 | BlackProfileModalImg.args = {
14 | profileUri: 'https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/atoms/ProfileModalImg/ProfileModalImg.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 |
5 | interface ProfileModalImgProps {
6 | profileUri: string;
7 | }
8 |
9 | const ProfileModalImgContainter = styled.div`
10 | display: flex;
11 | width: 18rem;
12 | height: 18rem;
13 | background-color: ${color.primary};
14 | `;
15 |
16 | const Img = styled.img`
17 | width: 100%;
18 | height: 100%;
19 | `;
20 |
21 | const ProfileModalImg: React.FC = ({ profileUri, ...props }) => {
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export { ProfileModalImg, ProfileModalImgProps };
30 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Text/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Text, TextProps } from './Text';
4 |
5 | export default {
6 | title: 'atom/Text',
7 | component: Text,
8 | argTypes: {
9 | color: { control: 'color' }
10 | }
11 | } as Meta;
12 |
13 | const Template: Story = (args) => ;
14 |
15 | export const ExampleText = Template.bind({});
16 | ExampleText.args = {
17 | children: 'Example'
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/components/atoms/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import { Size, Sizes } from '@constants/index';
2 | import { color } from '@theme/index';
3 | import React from 'react';
4 | import styled from 'styled-components';
5 |
6 | interface TextProps {
7 | size?: Sizes;
8 | children: React.ReactChild;
9 | isBold?: boolean;
10 | fontColor?: string;
11 | isSelect?: boolean;
12 | isTitle?: boolean;
13 | width?: string;
14 | isHover?: boolean;
15 | isEllipsis?: boolean;
16 | }
17 |
18 | const StyledText = styled.p`
19 | color: ${({ isSelect, isTitle, fontColor }) => (isSelect || isTitle ? color.text_secondary : fontColor)};
20 | font-size: ${({ size }) => {
21 | if (size === Size.BIG) return '3rem';
22 | if (size === Size.LARGE) return '1.5rem';
23 | if (size === Size.MEDIUM) return '1.3rem';
24 | if (size === Size.SMALL) return '1.0rem';
25 | return '0.8rem';
26 | }};
27 | font-weight: ${({ isBold }) => (isBold ? 'bold' : 'none')};
28 | margin: 0;
29 | width: ${({ width }) => width};
30 | ${({ isHover }) => isHover && `&:hover { text-decoration: underline }`}
31 | ${({ isEllipsis }) => isEllipsis && `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`}
32 | `;
33 |
34 | const Text: React.FC = ({
35 | children,
36 | size = Size.MEDIUM,
37 | fontColor = color.text_primary,
38 | isTitle = false,
39 | isBold = false,
40 | isSelect = false,
41 | width = 'auto',
42 | isHover = false,
43 | isEllipsis = false,
44 | ...props
45 | }) => {
46 | return (
47 |
57 | {children}
58 |
59 | );
60 | };
61 |
62 | export { Text, TextProps };
63 |
--------------------------------------------------------------------------------
/client/src/components/atoms/index.ts:
--------------------------------------------------------------------------------
1 | import { ActiveLight } from './ActiveLight/ActiveLight';
2 | import { Icon } from './Icon/Icon';
3 | import { ProfileImg } from './ProfileImg/ProfileImg';
4 | import { Text } from './Text/Text';
5 | import { Input } from './Input/Input';
6 | import { Button } from './Button/Button';
7 | import { LogoImg } from './LogoImg/LogoImg';
8 | import { ModalBox } from './ModalBox/ModalBox';
9 | import { HoverInput } from './HoverInput/HoverInput';
10 | import { Emoji } from './Emoji/Emoji';
11 | import { DropMenuBox } from './DropMenuBox/DropMenuBox';
12 | import { DropMenuItem } from './DropMenuItem/DropMenuItem';
13 | import { ProfileModalImg } from './ProfileModalImg/ProfileModalImg';
14 | import { ProfileModalBody } from './ProfileModalBody/ProfileModalBody';
15 |
16 | export {
17 | ActiveLight,
18 | Icon,
19 | ProfileImg,
20 | Text,
21 | Input,
22 | Button,
23 | LogoImg,
24 | ModalBox,
25 | HoverInput,
26 | Emoji,
27 | DropMenuBox,
28 | DropMenuItem,
29 | ProfileModalImg,
30 | ProfileModalBody
31 | };
32 |
--------------------------------------------------------------------------------
/client/src/components/molecules/Actionbar/Actionbar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Actionbar, ActionbarProps } from './Actionbar';
4 |
5 | export default {
6 | title: 'molecules/Actionbar',
7 | component: Actionbar
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackActionbar = Template.bind({});
13 | BlackActionbar.args = {
14 | chatId: 1
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/ActiveProfileImg/ActiveProfileImg.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import Logo from '@imgs/logo.png';
4 | import { Size } from '@constants/index';
5 | import { ActiveProfileImg, ActiveProfileImgProps } from './ActiveProfileImg';
6 |
7 | export default {
8 | title: 'molecules/ActiveProfileImg',
9 | component: ActiveProfileImg
10 | } as Meta;
11 |
12 | const Template: Story = (args) => ;
13 |
14 | export const LargeActiveProfileImg = Template.bind({});
15 | LargeActiveProfileImg.args = {
16 | size: Size.LARGE,
17 | src: Logo
18 | };
19 |
20 | export const MediumActiveProfileImg = Template.bind({});
21 | MediumActiveProfileImg.args = {
22 | size: Size.MEDIUM,
23 | src: Logo
24 | };
25 |
26 | export const SmallActiveProfileImg = Template.bind({});
27 | SmallActiveProfileImg.args = {
28 | size: Size.SMALL,
29 | src: Logo
30 | };
31 |
--------------------------------------------------------------------------------
/client/src/components/molecules/ActiveProfileImg/ActiveProfileImg.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { ActiveLight, ProfileImg } from '@components/atoms';
4 | import Logo from '@imgs/logo.png';
5 | import { color } from '@theme/index';
6 | import { Sizes, Size } from '@constants/index';
7 |
8 | interface ActiveProfileImgProps {
9 | size?: Sizes;
10 | isHover?: boolean;
11 | src?: string;
12 | isActive?: boolean;
13 | }
14 |
15 | const ActiveProfileImgContainter = styled.div`
16 | position: relative;
17 | width: ${({ size }) => {
18 | if (size === Size.LARGE) return '2.2rem';
19 | if (size === Size.MEDIUM) return '1.4rem';
20 | return '0.7rem';
21 | }};
22 | height: ${({ size }) => {
23 | if (size === Size.LARGE) return '2.2rem';
24 | if (size === Size.MEDIUM) return '1.4rem';
25 | return '0.7rem';
26 | }};
27 | `;
28 |
29 | const ActiveLightWrap = styled.div`
30 | position: absolute;
31 | right: -5px;
32 | bottom: -5px;
33 | border-color: ${color.primary};
34 | border-style: solid;
35 | border-radius: 1rem;
36 | `;
37 |
38 | const ActiveProfileImg: React.FC = ({ size = Size.MEDIUM, isActive = true, isHover = false, src = Logo, ...props }) => {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export { ActiveProfileImg, ActiveProfileImgProps };
50 |
--------------------------------------------------------------------------------
/client/src/components/molecules/AddChannelButton/AddChannelButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { AddChannelButton, AddChannelButtonProps } from './AddChannelButton';
4 |
5 | export default {
6 | title: 'molecules/AddChannelButton',
7 | component: AddChannelButton
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackAddChannelButton = Template.bind({});
13 | BlackAddChannelButton.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/molecules/AddChannelButton/AddChannelButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Plus from '@imgs/plus-icon.png';
4 | import { Icon } from '@components/atoms';
5 | import { useDispatch } from 'react-redux';
6 | import { channelModalOpen } from '@store/actions/modal-action';
7 | import { Size } from '@constants/index';
8 |
9 | interface AddChannelButtonProps {
10 | sectionName: string;
11 | setHover: React.Dispatch>;
12 | }
13 |
14 | const IconWrap = styled.div`
15 | display: flex;
16 | cursor: pointer;
17 | `;
18 |
19 | const AddChannelButton: React.FC = ({ setHover, sectionName, ...props }) => {
20 | const dispatch = useDispatch();
21 | const handlingHoverIconClick = (e: any) => {
22 | const x = window.pageXOffset + e.target.getBoundingClientRect().left;
23 | const y = window.pageYOffset + e.target.getBoundingClientRect().top;
24 | dispatch(channelModalOpen({ x, y }));
25 | };
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 | >
33 | );
34 | };
35 |
36 | export { AddChannelButton, AddChannelButtonProps };
37 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageChannelBody, BrowsePageChannelBodyProps } from './BrowsePageChannelBody';
4 |
5 | export default {
6 | title: 'molecules/BrowsePageChannelBody',
7 | component: BrowsePageChannelBody
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageChannelBody = Template.bind({});
13 | BlackBrowsePageChannelBody.args = {
14 | isJoined: true,
15 | members: 4,
16 | description: '공지사항을 안내하는 채널'
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Text } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { Size } from '@constants/index';
6 |
7 | interface BrowsePageChannelBodyProps {
8 | isJoined?: boolean;
9 | members: number;
10 | description?: string;
11 | }
12 |
13 | const BrowsePageChannelBodyWrap = styled.div`
14 | display: flex;
15 | p {
16 | margin-right: 0.3rem;
17 | }
18 | `;
19 |
20 | const BrowsePageChannelBody: React.FC = ({ isJoined, members, description, ...props }) => {
21 | return (
22 |
23 | {isJoined && (
24 |
25 | {`✓ Joined ·`}
26 |
27 | )}
28 |
29 | {`${members} members`}
30 |
31 | {description && (
32 |
33 | {`· ${description}`}
34 |
35 | )}
36 |
37 | );
38 | };
39 |
40 | export { BrowsePageChannelBody, BrowsePageChannelBodyProps };
41 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageChannelButton, BrowsePageChannelButtonProps } from './BrowsePageChannelButton';
4 |
5 | export default {
6 | title: 'molecules/BrowsePageChannelButton',
7 | component: BrowsePageChannelButton
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageChannelButton = Template.bind({});
13 | BlackBrowsePageChannelButton.args = {
14 | isJoined: true
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@components/atoms';
3 | import { color } from '@theme/index';
4 |
5 | interface BrowsePageChannelButtonProps {
6 | isJoined?: boolean;
7 | handlingJoinButton?: () => void;
8 | handlingLeaveButton?: () => void;
9 | }
10 |
11 | const BrowsePageChannelButton: React.FC = ({ isJoined, handlingJoinButton, handlingLeaveButton, ...props }) => {
12 | return (
13 | <>
14 | {isJoined ? (
15 |
24 | ) : (
25 |
34 | )}
35 | >
36 | );
37 | };
38 |
39 | export { BrowsePageChannelButton, BrowsePageChannelButtonProps };
40 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageChannelHeader, BrowsePageChannelHeaderProps } from './BrowsePageChannelHeader';
4 |
5 | export default {
6 | title: 'molecules/BrowsePageChannelHeader',
7 | component: BrowsePageChannelHeader
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageChannelHeader = Template.bind({});
13 | BlackBrowsePageChannelHeader.args = {
14 | title: 'notice',
15 | isPrivate: true
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Icon, Text } from '@components/atoms';
4 | import ChannelIcon from '@imgs/channel-icon.png';
5 | import LockIcon from '@imgs/lock-icon.png';
6 | import { color } from '@theme/index';
7 | import { Size } from '@constants/index';
8 |
9 | interface BrowsePageChannelHeaderProps {
10 | title: string;
11 | isPrivate?: boolean;
12 | }
13 |
14 | const BrowsePageChannelHeaderWrap = styled.div`
15 | display: flex;
16 | p {
17 | margin-left: 0.3rem;
18 | }
19 | `;
20 |
21 | const BrowsePageChannelHeader: React.FC = ({ title, isPrivate, ...props }) => {
22 | return (
23 |
24 |
25 |
26 | {title}
27 |
28 |
29 | );
30 | };
31 |
32 | export { BrowsePageChannelHeader, BrowsePageChannelHeaderProps };
33 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageControls/BrowsePageControls.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageControls, BrowsePageControlsProps } from './BrowsePageControls';
4 |
5 | export default {
6 | title: 'molecules/BrowsePageControls',
7 | component: BrowsePageControls
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageControls = Template.bind({});
13 | BlackBrowsePageControls.args = {
14 | channelCount: 193
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Text } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { WhiteButtonWithIcon } from '@components/molecules';
6 | import SortIcon from '@imgs/sort-icon.png';
7 | import FilterIcon from '@imgs/filter-icon.png';
8 | import { Size, SortMethod, SortMethods } from '@constants/index';
9 |
10 | interface BrowsePageControlsProps {
11 | channelCount: number;
12 | }
13 |
14 | const BrowsePageControlsWrap = styled.div`
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 | padding-top: 0.4rem;
19 | padding-bottom: 1rem;
20 | margin-left: 1.5rem;
21 | margin-right: 2.5rem;
22 | border-bottom: 0.5px solid ${color.border_secondary};
23 | `;
24 |
25 | const BrowsePageControlsButtonWrap = styled.div`
26 | display: flex;
27 | `;
28 |
29 | const BrowsePageControls: React.FC = ({ channelCount, ...props }) => {
30 | const [sortMethod, setSortMethod] = useState(SortMethod.A_TO_Z);
31 |
32 | const handlingSortButton = () => {};
33 | const handlingFilterButton = () => {};
34 |
35 | return (
36 |
37 |
38 | {`${channelCount} channels`}
39 |
40 |
41 |
42 | {`Sort: ${sortMethod}`}
43 |
44 |
45 | Filter
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export { BrowsePageControls, BrowsePageControlsProps };
53 |
--------------------------------------------------------------------------------
/client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageSearchBar, BrowsePageSearchBarProps } from './BrowsePageSearchBar';
4 |
5 | export default {
6 | title: 'molecules/BrowsePageSearchBar',
7 | component: BrowsePageSearchBar
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageSearchBar = Template.bind({});
13 | BlackBrowsePageSearchBar.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/molecules/Channel/Channel.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Channel, ChannelProps } from './Channel';
4 |
5 | export default {
6 | title: 'molecules/Channel',
7 | component: Channel
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackChannel = Template.bind({});
13 | BlackChannel.args = {
14 | children: '5주-그룹-프로젝트-슬랙'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/ChannelModal/ChannelModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ChannelModal, ChannelModalProps } from './ChannelModal';
4 |
5 | export default {
6 | title: 'molecules/ChannelModal',
7 | component: ChannelModal
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackChannelModal = Template.bind({});
13 | BlackChannelModal.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/molecules/ChannelModal/ChannelModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropMenuBox, DropMenuItem } from '@components/atoms';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { useHistory } from 'react-router-dom';
5 | import { createModalOpen, channelModalClose } from '@store/actions/modal-action';
6 | import { resetSelectedChannel } from '@store/actions/chatroom-action';
7 | import styled from 'styled-components';
8 |
9 | interface ChannelModalProps {}
10 |
11 | const DropMenuItemContainer = styled.div`
12 | padding: 1rem 0rem;
13 | `;
14 |
15 | const ChannelModal: React.FC = ({ ...props }) => {
16 | const history = useHistory();
17 | const dispatch = useDispatch();
18 | const { x, y } = useSelector((store: any) => store.modal.channelModal);
19 |
20 | const isOpen = useSelector((store: any) => store.modal.channelModal.isOpen);
21 |
22 | const handlingCloseModal = () => {
23 | dispatch(channelModalClose());
24 | };
25 | const handlingBrowseChannelsClick = () => {
26 | if (window.location.pathname !== `/channel-browser`) history.push(`/channel-browser`);
27 | dispatch(resetSelectedChannel());
28 |
29 | dispatch(channelModalClose());
30 | };
31 | const handlingCreateChannelClick = () => {
32 | dispatch(createModalOpen());
33 | };
34 | return (
35 | <>
36 | {isOpen ? (
37 | handlingCloseModal()} {...props}>
38 |
39 | Browse channels
40 | Create a channel
41 |
42 |
43 | ) : null}
44 | >
45 | );
46 | };
47 |
48 | export { ChannelModal, ChannelModalProps };
49 |
--------------------------------------------------------------------------------
/client/src/components/molecules/DM/DM.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { DM, DMProps } from './DM';
4 |
5 | export default {
6 | title: 'molecules/DM',
7 | component: DM
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackDM = Template.bind({});
13 | BlackDM.args = {
14 | children: 'J030_김도호'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/DM/DM.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Text } from '@components/atoms';
4 | import { ActiveProfileImg } from '@components/molecules';
5 | import { useHistory } from 'react-router-dom';
6 | import { color } from '@theme/index';
7 | import { useDispatch } from 'react-redux';
8 | import { pickChannel } from '@store/actions/chatroom-action';
9 | import { getThreadId } from '@utils/uriParser';
10 | import { Size } from '@constants/index';
11 |
12 | interface DMProps {
13 | children: React.ReactChild;
14 | isSelect?: boolean;
15 | src?: string;
16 | chatroomId: number;
17 | }
18 |
19 | const DMContainter = styled.div<{ isSelect: boolean }>`
20 | display: flex;
21 | align-items: center;
22 | padding: 0.4rem 1rem;
23 | cursor: pointer;
24 | ${(props) => (props.isSelect ? `background-color: ${color.selected_chatroom}` : `&:hover { background-color: ${color.primary};}`)}
25 | `;
26 |
27 | const TextWrap = styled.div`
28 | margin-left: 1rem;
29 | width: -webkit-fill-available;
30 | `;
31 |
32 | const DM: React.FC = ({ children, src, chatroomId, isSelect = false, ...props }) => {
33 | const history = useHistory();
34 | const dispatch = useDispatch();
35 |
36 | const handlingClick = () => {
37 | const threadId = getThreadId();
38 | const pathname = threadId ? `/client/${chatroomId}/thread/${threadId}` : `/client/${chatroomId}`;
39 | if (window.location.pathname !== pathname) history.push(pathname);
40 | dispatch(pickChannel({ selectedChatroomId: chatroomId }));
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export { DM, DMProps };
54 |
--------------------------------------------------------------------------------
/client/src/components/molecules/EmojiBox/EmojiBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { EmojiBox, EmojiBoxProps } from './EmojiBox';
4 |
5 | export default {
6 | title: 'molecules/EmojiBox',
7 | component: EmojiBox
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackEmojiBox = Template.bind({});
13 | BlackEmojiBox.args = {
14 | emoji: '✔',
15 | number: 1
16 | };
17 |
18 | export const HeartEmojiBox = Template.bind({});
19 | HeartEmojiBox.args = {
20 | emoji: '❤',
21 | number: 1
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/components/molecules/EmojiBox/EmojiBox.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 | import React, { useState } from 'react';
3 | import styled from 'styled-components';
4 | import { Emoji } from '@components/atoms';
5 | import { color } from '@theme/index';
6 |
7 | interface EmojiBoxProps {
8 | emoji: string;
9 | active?: boolean;
10 | number: number;
11 | reactionId: number;
12 | title: string;
13 | createReaction: any;
14 | deleteReaction: any;
15 | }
16 |
17 | const EmojiBoxContainer = styled.div<{ isActive: boolean }>`
18 | display: flex;
19 | justify-content: space-evenly;
20 | align-items: center;
21 | padding-bottom: 0.2rem;
22 | width: 3rem;
23 | background-color: ${(props) => (props.isActive ? color.emoji_bg : color.quaternary)};
24 | border-radius: 1rem;
25 | cursor: pointer;
26 | margin-right: 0.3rem;
27 | box-shadow: 0 0 0 1px inset ${(props) => (props.isActive ? color.border_tertiary : color.quaternary)};
28 |
29 | ${(props) =>
30 | props.isActive
31 | ? null
32 | : `&:hover {
33 | background-color: ${color.tertiary};
34 | box-shadow: 0 0 0 1px inset;`}}
35 | `;
36 |
37 | const EmojiBoxText = styled.p`
38 | font-size: 0.9rem;
39 | margin: 0;
40 | `;
41 |
42 | const EmojiBox: React.FC = ({ reactionId, title, createReaction, deleteReaction, active = false, number, emoji, ...props }) => {
43 | const [isActive, setActive] = useState(active);
44 |
45 | const handlingClick = () => {
46 | isActive ? deleteReaction(reactionId) : createReaction(title, emoji);
47 | setActive(!isActive);
48 | };
49 |
50 | return (
51 |
52 |
53 | {number}
54 |
55 | );
56 | };
57 |
58 | export { EmojiBox, EmojiBoxProps };
59 |
--------------------------------------------------------------------------------
/client/src/components/molecules/EmojiPicker/EmojiPicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { EmojiPicker, EmojiPickerProps } from './EmojiPicker';
4 |
5 | export default {
6 | title: 'atom/EmojiPicker',
7 | component: EmojiPicker
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackEmojiPicker = Template.bind({});
13 | BlackEmojiPicker.args = {
14 | onEmojiClick: () => {}
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/EmojiPicker/EmojiPicker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Picker from 'emoji-picker-react';
3 | import { DropMenuBox } from '@components/atoms';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { emojiPickerClose } from '@store/actions/modal-action';
6 | import { createMessageReaction, createReplyReaction } from '@socket/emits/reaction';
7 | import { ChatType } from '@constants/index';
8 |
9 | interface EmojiPickerProps {}
10 |
11 | const EmojiPicker: React.FC = () => {
12 | const dispatch = useDispatch();
13 | const { x, y, chatId, isOpen, type } = useSelector((store: any) => store.modal.emojiPicker);
14 |
15 | const onEmojiClick = (e: any, emojiObject: any) => {
16 | const { names, emoji } = emojiObject;
17 | if (type === ChatType.Reply) createReplyReaction({ replyId: chatId, title: names[0], emoji });
18 | else createMessageReaction({ messageId: chatId, title: names[0], emoji });
19 | dispatch(emojiPickerClose());
20 | };
21 |
22 | const handlingCloseModal = () => {
23 | dispatch(emojiPickerClose());
24 | };
25 |
26 | return (
27 | <>
28 | {isOpen && (
29 |
30 |
31 |
32 | )}
33 | >
34 | );
35 | };
36 |
37 | export { EmojiPicker, EmojiPickerProps };
38 |
--------------------------------------------------------------------------------
/client/src/components/molecules/GithubLoginButton/GithubLoginButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { GithubLoginButton, GithubLoginButtonProps } from './GithubLoginButton';
4 |
5 | export default {
6 | title: 'molecules/GithubLoginButton',
7 | component: GithubLoginButton
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackGithubLoginButton = Template.bind({});
13 | BlackGithubLoginButton.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/molecules/GithubLoginButton/GithubLoginButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Icon, Text } from '@components/atoms';
3 | import styled from 'styled-components';
4 | import { API } from '@utils/index';
5 | import { color } from '@theme/index';
6 |
7 | interface GithubLoginButtonProps {}
8 |
9 | const TextWrap = styled.div`
10 | margin-left: 1rem;
11 | `;
12 |
13 | const handlingGithubLoginButton = async () => {
14 | await API.oauthLogin();
15 | };
16 |
17 | const GithubLoginButton: React.FC = ({ ...props }) => {
18 | return (
19 |
25 | );
26 | };
27 |
28 | export { GithubLoginButton, GithubLoginButtonProps };
29 |
--------------------------------------------------------------------------------
/client/src/components/molecules/HoverIcon/HoverIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import Plus from '@imgs/plus-icon.png';
4 | import { Size } from '@constants/index';
5 | import { HoverIcon, HoverIconProps } from './HoverIcon';
6 |
7 | export default {
8 | title: 'molecules/HoverIcon',
9 | component: HoverIcon
10 | } as Meta;
11 |
12 | const Template: Story = (args) => ;
13 |
14 | export const MediumHoverIcon = Template.bind({});
15 | MediumHoverIcon.args = {
16 | src: Plus,
17 | size: Size.MEDIUM
18 | };
19 |
20 | export const LargeHoverIcon = Template.bind({});
21 | LargeHoverIcon.args = {
22 | src: Plus,
23 | size: Size.LARGE
24 | };
25 |
--------------------------------------------------------------------------------
/client/src/components/molecules/HoverIcon/HoverIcon.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React from 'react';
3 | import styled from 'styled-components';
4 | import { Icon } from '@components/atoms';
5 | import { color } from '@theme/index';
6 | import { Sizes, Size } from '@constants/index';
7 |
8 | interface HoverIconProps {
9 | size: Sizes;
10 | src?: string;
11 | onClick?: any;
12 | }
13 |
14 | const StyledHoverIcon = styled.div`
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | width: ${({ size }) => {
19 | if (size === Size.LARGE) return '2.4rem';
20 | if (size === Size.MEDIUM) return '2.0rem';
21 | return '1.8rem';
22 | }};
23 | height: ${({ size }) => {
24 | if (size === Size.LARGE) return '2.4rem';
25 | if (size === Size.MEDIUM) return '2.0rem';
26 | return '1.8rem';
27 | }};
28 | border-radius: 0.4rem;
29 | cursor: pointer;
30 | &:hover {
31 | background-color: ${color.hover_primary};
32 | }
33 | `;
34 |
35 | const HoverIcon: React.FC = ({ size = Size.MEDIUM, src, ...props }) => {
36 | return (
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export { HoverIcon, HoverIconProps };
44 |
--------------------------------------------------------------------------------
/client/src/components/molecules/InputMessage/InputMessage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { InputMessage, InputMessageProps } from './InputMessage';
4 |
5 | export default {
6 | title: 'molecules/InputMessage',
7 | component: InputMessage
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const MessageInputMessage = Template.bind({});
13 | MessageInputMessage.args = {
14 | isThread: false,
15 | title: '5주-그룹-프로젝트-슬랙b'
16 | };
17 |
18 | export const ThreadInputMessage = Template.bind({});
19 | ThreadInputMessage.args = {
20 | isThread: true
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/components/molecules/InputMessage/InputMessage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Input } from '@components/atoms';
4 | import { SendMessageButton } from '@components/molecules';
5 | import { color } from '@theme/index';
6 | import { createMessage } from '@socket/emits/message';
7 | import { ChatType, ScrollEventType } from '@constants/index';
8 |
9 | interface InputMessageProps {
10 | isThread?: boolean;
11 | title: string;
12 | setEventType: any;
13 | chatroomId: number;
14 | }
15 |
16 | const InputMessageContainer = styled.div`
17 | display: flex;
18 | justify-content: space-between;
19 | align-items: center;
20 | border: 1px solid ${color.primary};
21 | padding: 0.5rem 0rem;
22 | width: 100%;
23 | max-height: 70%;
24 | border-radius: 0.3rem;
25 | `;
26 |
27 | const InputWrap = styled.div`
28 | margin-left: 1rem;
29 | width: 70%;
30 | `;
31 |
32 | const ButtonWrap = styled.div`
33 | margin-right: 1rem;
34 | `;
35 |
36 | const InputMessage: React.FC = ({ children, title, isThread, chatroomId, setEventType, ...props }) => {
37 | const [content, setContent] = useState('');
38 |
39 | const sendMessage = () => {
40 | if (content === '') return;
41 | setEventType(ScrollEventType.INPUTTEXT);
42 | createMessage({ content, chatroomId });
43 | setContent('');
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export { InputMessage, InputMessageProps };
59 |
--------------------------------------------------------------------------------
/client/src/components/molecules/InputReply/InputReply.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { InputReply, InputReplyProps } from './InputReply';
4 |
5 | export default {
6 | title: 'molecules/InputReply',
7 | component: InputReply
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const ThreadInputReply = Template.bind({});
13 | ThreadInputReply.args = {
14 | isThread: true
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/molecules/InputReply/InputReply.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Input } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { SendMessageButton } from '@components/molecules';
6 | import { createReply } from '@socket/emits/thread';
7 | import { ScrollEventType, ChatType } from '@constants/index';
8 |
9 | interface InputReplyProps {
10 | isThread?: boolean;
11 | messageId: number | null;
12 | setEventType: any;
13 | }
14 |
15 | const InputReplyContainer = styled.div`
16 | display: flex;
17 | justify-content: space-between;
18 | align-items: center;
19 | border: 1px solid ${color.primary};
20 | padding: 0.5rem 0rem;
21 | width: 100%;
22 | max-height: 70%;
23 | border-radius: 0.3rem;
24 | `;
25 |
26 | const InputWrap = styled.div`
27 | margin-left: 1rem;
28 | width: 70%;
29 | `;
30 |
31 | const ButtonWrap = styled.div`
32 | margin-right: 1rem;
33 | `;
34 |
35 | const InputReply: React.FC = ({ children, messageId, isThread, setEventType, ...props }) => {
36 | const [content, setContent] = useState('');
37 | const sendReply = () => {
38 | if (content === '') return;
39 | setEventType(ScrollEventType.INPUTTEXT);
40 | createReply({ content, messageId });
41 | setContent('');
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export { InputReply, InputReplyProps };
57 |
--------------------------------------------------------------------------------
/client/src/components/molecules/Message/Message.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Message, MessageProps } from './Message';
4 |
5 | export default {
6 | title: 'molecules/Message',
7 | component: Message
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackMessage = Template.bind({});
13 | BlackMessage.args = {
14 | src: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4',
15 | author: 'J030_김도호',
16 | content:
17 | '안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. 안녕하세요. 김도호입니다. '
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/components/molecules/MessageReplyBar/MessageReplyBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { MessageReplyBar, MessageReplyBarProps } from './MessageReplyBar';
4 |
5 | export default {
6 | title: 'molecules/MessageReplyBar',
7 | component: MessageReplyBar
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | const handlingMessageReplyBarClick = () => {};
13 |
14 | export const OneMessageReplyBar = Template.bind({});
15 | OneMessageReplyBar.args = {
16 | profileImgs: ['https://avatars1.githubusercontent.com/u/59037261?s=64&v=4'],
17 | replyCount: 1,
18 | lastRepliedTime: new Date('2020-12-07 23:48:52.547160'),
19 | onClick: handlingMessageReplyBarClick
20 | };
21 |
22 | export const ThreeMessageReplyBar = Template.bind({});
23 | ThreeMessageReplyBar.args = {
24 | profileImgs: [
25 | 'https://avatars3.githubusercontent.com/u/33643752?s=64&v=4',
26 | 'https://avatars1.githubusercontent.com/u/59037261?s=64&v=4',
27 | 'https://avatars0.githubusercontent.com/u/37091190?s=64&v=4'
28 | ],
29 | replyCount: 3,
30 | lastRepliedTime: new Date('2020-12-08 08:32:51.536510'),
31 | onClick: handlingMessageReplyBarClick
32 | };
33 |
--------------------------------------------------------------------------------
/client/src/components/molecules/MessageReplyBar/MessageReplyBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { ProfileImg, Text } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { timeAgo } from '@utils/time';
6 | import { Size } from '@constants/index';
7 |
8 | interface MessageReplyBarProps {
9 | profileImgs: Array;
10 | replyCount: number;
11 | lastRepliedTime: Date;
12 | onClick?: () => void;
13 | }
14 |
15 | const MessageReplyBarWrap = styled.div`
16 | display: flex;
17 | margin-top: 0.2rem;
18 | padding: 0.2rem;
19 | border-radius: 0.3rem;
20 | cursor: pointer;
21 | &:hover {
22 | background-color: ${color.tertiary};
23 | }
24 | `;
25 |
26 | const ProfileImgWrap = styled.div`
27 | margin: 0rem 0.05rem;
28 | `;
29 |
30 | const ReplyCountWrap = styled.div`
31 | display: flex;
32 | margin: 0rem 0.5rem;
33 | p:hover {
34 | text-decoration: underline;
35 | }
36 | `;
37 |
38 | const MessageReplyBar: React.FC = ({ profileImgs, replyCount, lastRepliedTime, onClick, ...props }) => {
39 | const profileNum = profileImgs.length >= 5 ? 5 : profileImgs.length;
40 | const removedOverlapProfileImgs = Array.from(new Set(profileImgs));
41 | const createProfileImg = removedOverlapProfileImgs.slice(0, profileNum).map((profileImg) => (
42 |
43 |
44 |
45 | ));
46 |
47 | return (
48 |
49 | {createProfileImg}
50 |
51 |
52 | {`${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}`}
53 |
54 |
55 |
56 | {timeAgo(lastRepliedTime)}
57 |
58 |
59 | );
60 | };
61 |
62 | export { MessageReplyBar, MessageReplyBarProps };
63 |
--------------------------------------------------------------------------------
/client/src/components/molecules/ProfileModal/ProfileModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ProfileModal, ProfileModalProps } from './ProfileModal';
4 |
5 | export default {
6 | title: 'molecules/ProfileModal',
7 | component: ProfileModal
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackProfileModal = Template.bind({});
13 | BlackProfileModal.args = {
14 | userId: 1,
15 | profileUri: 'https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4',
16 | displayName: 'profornnan'
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/molecules/Section/Section.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Channel, DM } from '@components/molecules';
4 | import { Section, SectionProps } from './Section';
5 |
6 | export default {
7 | title: 'molecules/Section',
8 | component: Section
9 | } as Meta;
10 |
11 | const mockChannelChildren = ['5주-그룹-프로젝트-슬랙', 'boost-ajae', '어몽어스'].map((item) => {
12 | return {item};
13 | });
14 |
15 | const mockDMChildren = ['J003_강동훈', 'J030_김도호', 'J211_탁성건'].map((item) => {
16 | return {item};
17 | });
18 |
19 | const Template: Story = (args) => ;
20 |
21 | export const ChannelSection = Template.bind({});
22 | ChannelSection.args = {
23 | sectionName: 'Channels',
24 | children: mockChannelChildren
25 | };
26 |
27 | export const DMSection = Template.bind({});
28 | DMSection.args = {
29 | sectionName: 'Direct Messages',
30 | children: mockDMChildren
31 | };
32 |
--------------------------------------------------------------------------------
/client/src/components/molecules/Section/Section.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { DefaultSectionName } from '@constants/index';
4 | import { AddChannelButton } from '../AddChannelButton/AddChannelButton';
5 |
6 | interface SectionProps {
7 | children: React.ReactNode;
8 | sectionName: string;
9 | isSelect?: boolean;
10 | }
11 |
12 | const SectionWrap = styled.div`
13 | position: relative;
14 | `;
15 |
16 | const AddChannelButtonWrap = styled.div`
17 | position: absolute;
18 | top: 0;
19 | right: 0.5rem;
20 | `;
21 |
22 | const StyledSection = styled.details`
23 | color: rgb(198, 199, 200);
24 | font-size: 1rem;
25 | margin-bottom: 1rem;
26 | `;
27 |
28 | const Summary = styled.summary`
29 | outline: none;
30 | cursor: pointer;
31 | margin-bottom: 0.3rem;
32 | `;
33 |
34 | const Section: React.FC = ({ children, sectionName = 'Section', isSelect = false, ...props }) => {
35 | const [isHover, setHover] = useState(false);
36 |
37 | const onMouseEnter = () => {
38 | setHover(true);
39 | };
40 |
41 | const onMouseLeave = () => {
42 | setHover(false);
43 | };
44 |
45 | return (
46 |
47 | {isHover && DefaultSectionName.CHANNELS === sectionName && (
48 |
49 |
50 |
51 | )}
52 |
53 |
54 | {sectionName}
55 |
56 | {children}
57 |
58 |
59 | );
60 | };
61 |
62 | export { Section, SectionProps };
63 |
--------------------------------------------------------------------------------
/client/src/components/molecules/SendMessageButton/SendMessageButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { SendMessageButton, SendMessageButtonProps } from './SendMessageButton';
4 |
5 | export default {
6 | title: 'molecules/SendMessageButton',
7 | component: SendMessageButton
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const ActiveMessageButton = Template.bind({});
13 | ActiveMessageButton.args = {
14 | isActive: true
15 | };
16 |
17 | export const InactiveMessageButton = Template.bind({});
18 | InactiveMessageButton.args = {
19 | isActive: false
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/components/molecules/SendMessageButton/SendMessageButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Icon } from '@components/atoms';
4 | import SendMessageIcon from '@imgs/send-message-icon.png';
5 | import { color } from '@theme/index';
6 | import { Size } from '@constants/index';
7 |
8 | interface SendMessageButtonProps {
9 | isActive: boolean;
10 | sendMessage: () => void;
11 | }
12 |
13 | const SendMessageButtonContainer = styled.div<{ isActive: boolean }>`
14 | display: flex;
15 | justify-content: center;
16 | align-items: baseline;
17 | width: 1.3rem;
18 | padding: 0.3rem;
19 | border-radius: 0.3rem;
20 | cursor: pointer;
21 | ${(props) => (props.isActive ? `background-color: ${color.button_secondary}` : '')}
22 | `;
23 |
24 | const SendMessageButton: React.FC = ({ isActive, sendMessage, ...props }) => {
25 | const handlingClick = () => {
26 | sendMessage();
27 | };
28 | return (
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export { SendMessageButton, SendMessageButtonProps };
36 |
--------------------------------------------------------------------------------
/client/src/components/molecules/UserBox/UserBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { UserBox, UserBoxProps } from './UserBox';
4 |
5 | export default {
6 | title: 'molecules/UserBox',
7 | component: UserBox
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const OneUserBox = Template.bind({});
13 | OneUserBox.args = {
14 | member: [{ name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' }]
15 | };
16 |
17 | export const TwoUserBox = Template.bind({});
18 | TwoUserBox.args = {
19 | member: [
20 | { name: '강동훈', profileUri: 'https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4' },
21 | { name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' }
22 | ]
23 | };
24 |
25 | export const ThreeUserBox = Template.bind({});
26 | ThreeUserBox.args = {
27 | member: [
28 | { name: '강동훈', profileUri: 'https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4' },
29 | { name: '김도호', profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4' },
30 | { name: '탁성건', profileUri: 'https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4' }
31 | ]
32 | };
33 |
--------------------------------------------------------------------------------
/client/src/components/molecules/UserBox/UserBox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { ProfileImg } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { userboxModalOpen } from '@store/actions/modal-action';
6 | import { useDispatch } from 'react-redux';
7 |
8 | interface UserBoxProps {
9 | member: Array;
10 | }
11 |
12 | const UserBoxWrap = styled.div`
13 | display: flex;
14 | align-items: center;
15 | padding-right: 1rem;
16 | border-radius: 0.2rem;
17 | &:hover {
18 | background-color: ${color.hover_primary};
19 | }
20 | cursor: pointer;
21 | `;
22 |
23 | const ProfileImgWrap = styled.div`
24 | border: 2px solid white;
25 | border-radius: 0.5rem;
26 | margin-left: -0.4rem;
27 | `;
28 |
29 | const Text = styled.div`
30 | margin-left: 0.7rem;
31 | color: ${color.text_senary};
32 | font-weight: 600;
33 | `;
34 |
35 | const UserBox: React.FC = ({ member, ...props }) => {
36 | const dispatch = useDispatch();
37 | const memberNum = member.length;
38 | const displayMembers: object[] = new Array(3);
39 | const loopValue = memberNum > 3 ? 3 : memberNum;
40 |
41 | for (let i = 0; i < loopValue; i += 1) {
42 | displayMembers.push({ id: member[i].userId, profileUri: member[i].profileUri });
43 | }
44 |
45 | const createProfileImg = displayMembers.map((Member: any) => (
46 |
47 |
48 |
49 | ));
50 |
51 | return (
52 | dispatch(userboxModalOpen())} {...props}>
53 | {createProfileImg}
54 | {memberNum}
55 |
56 | );
57 | };
58 |
59 | export { UserBox, UserBoxProps };
60 |
--------------------------------------------------------------------------------
/client/src/components/molecules/UserBoxModalSearchBar/UserBoxModalSearchBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { UserBoxModalSearchBar, UserBoxModalSearchBarProps } from './UserBoxModalSearchBar';
4 |
5 | export default {
6 | title: 'molecules/UserBoxModalSearchBar',
7 | component: UserBoxModalSearchBar
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackUserBoxModalSearchBar = Template.bind({});
13 | BlackUserBoxModalSearchBar.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/molecules/UserBoxModalUserItem/UserBoxModalUserItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { UserBoxModalUserItem, UserBoxModalUserItemProps } from './UserBoxModalUserItem';
4 |
5 | export default {
6 | title: 'molecules/UserBoxModalUserItem',
7 | component: UserBoxModalUserItem
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackUserBoxModalUserItem = Template.bind({});
13 | BlackUserBoxModalUserItem.args = {
14 | user: {
15 | displayName: 'J030_김도호',
16 | profileUri: 'https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4',
17 | userId: 1
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/client/src/components/molecules/UserBoxModalUserItem/UserBoxModalUserItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { ProfileImg, Text } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { openProfileModal } from '@utils/modal';
6 | import { Size } from '@constants/index';
7 |
8 | interface User {
9 | displayName: string;
10 | profileUri: string;
11 | userId: number;
12 | }
13 |
14 | interface UserBoxModalUserItemProps {
15 | user: User;
16 | }
17 |
18 | const UserBoxModalUserItemContainer = styled.div`
19 | display: flex;
20 | align-items: center;
21 | padding: 0.5rem 0;
22 | &:hover {
23 | background-color: ${color.hover_primary};
24 | }
25 | `;
26 |
27 | const ProfileImgWrap = styled.div`
28 | cursor: pointer;
29 | `;
30 |
31 | const TextWrap = styled.div`
32 | margin-left: 0.5rem;
33 | cursor: pointer;
34 | `;
35 |
36 | const UserBoxModalUserItem: React.FC = ({ user, ...props }) => {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 | {user.displayName}
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export { UserBoxModalUserItem, UserBoxModalUserItemProps };
52 |
--------------------------------------------------------------------------------
/client/src/components/molecules/WhiteButtonWithIcon/WhiteButtonWithIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import SortIcon from '@imgs/sort-icon.png';
4 | import { WhiteButtonWithIcon, WhiteButtonWithIconProps } from './WhiteButtonWithIcon';
5 |
6 | export default {
7 | title: 'molecules/WhiteButtonWithIcon',
8 | component: WhiteButtonWithIcon
9 | } as Meta;
10 |
11 | const onClick = () => {};
12 |
13 | const Template: Story = (args) => ;
14 |
15 | export const BlackWhiteButtonWithIcon = Template.bind({});
16 | BlackWhiteButtonWithIcon.args = {
17 | children: 'Sort',
18 | iconSrc: SortIcon,
19 | onClick
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/components/molecules/WhiteButtonWithIcon/WhiteButtonWithIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Button, Text, Icon } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { Size } from '@constants/index';
6 |
7 | interface WhiteButtonWithIconProps {
8 | children: React.ReactChild;
9 | iconSrc: string;
10 | onClick: () => void;
11 | }
12 |
13 | const WhiteButtonWithIconWrap = styled.div`
14 | display: flex;
15 | width: fit-content;
16 | p {
17 | margin-left: 0.3rem;
18 | }
19 | `;
20 |
21 | const WhiteButtonWithIcon: React.FC = ({ children, iconSrc, onClick, ...props }) => {
22 | return (
23 |
24 |
36 |
37 | );
38 | };
39 |
40 | export { WhiteButtonWithIcon, WhiteButtonWithIconProps };
41 |
--------------------------------------------------------------------------------
/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageChannel, BrowsePageChannelProps } from './BrowsePageChannel';
4 |
5 | export default {
6 | title: 'organisms/BrowsePageChannel',
7 | component: BrowsePageChannel
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageChannel = Template.bind({});
13 | BlackBrowsePageChannel.args = {
14 | chatroomId: 1,
15 | title: 'notice',
16 | isJoined: true,
17 | members: 4,
18 | description: '공지사항을 안내하는 채널',
19 | isPrivate: true
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageChannelList, BrowsePageChannelListProps } from './BrowsePageChannelList';
4 |
5 | export default {
6 | title: 'organisms/BrowsePageChannelList',
7 | component: BrowsePageChannelList
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackBrowsePageChannelList = Template.bind({});
13 | BlackBrowsePageChannelList.args = {
14 | channels: [
15 | {
16 | chatroomId: 1,
17 | title: 'notice',
18 | description: '공지사항을 안내하는 채널',
19 | isPrivate: false,
20 | members: 110,
21 | isJoined: true
22 | },
23 | {
24 | chatroomId: 2,
25 | title: '질의응답',
26 | isPrivate: false,
27 | members: 10,
28 | isJoined: false
29 | },
30 | {
31 | chatroomId: 3,
32 | title: 'black',
33 | description: 'black',
34 | isPrivate: true,
35 | members: 3,
36 | isJoined: true
37 | }
38 | ]
39 | };
40 |
--------------------------------------------------------------------------------
/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import { BrowsePageChannel } from '@components/organisms';
4 | import { useDispatch } from 'react-redux';
5 | import { ChannelState } from '@store/types/channel-types';
6 | import { loadNextChannels } from '@store/actions/channel-action';
7 |
8 | interface BrowsePageChannelListProps {
9 | channels: Array;
10 | }
11 |
12 | const BrowsePageChannelListContainter = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 | overflow-y: scroll;
16 | padding: 0rem 1.5rem;
17 | height: 71%;
18 | `;
19 |
20 | const BrowsePageChannelList: React.FC = ({ channels, ...props }) => {
21 | const dispatch = useDispatch();
22 | const [lastRequestChannelTitle, setLastRequestChannelTitle] = useState('');
23 | const onScrollHandler = (e: any) => {
24 | const title: string | null = channels[channels.length - 1]?.title;
25 | if (e.target.scrollTop >= ((e.target.scrollHeight - e.target.clientHeight) * 2) / 3) {
26 | if (title === lastRequestChannelTitle) return;
27 | dispatch(loadNextChannels({ title }));
28 | setLastRequestChannelTitle(title);
29 | }
30 | };
31 |
32 | const createMessages = () => {
33 | return channels.map((channel: any) => (
34 |
43 | ));
44 | };
45 |
46 | return (
47 |
48 | {createMessages()}
49 |
50 | );
51 | };
52 |
53 | export { BrowsePageChannelList, BrowsePageChannelListProps };
54 |
--------------------------------------------------------------------------------
/client/src/components/organisms/BrowsePageHeader/BrowsePageHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { BrowsePageHeader, BrowsePageHeaderProps } from './BrowsePageHeader';
4 |
5 | export default {
6 | title: 'organisms/BrowsePageHeader',
7 | component: BrowsePageHeader
8 | } as Meta;
9 |
10 | const onClick = () => {};
11 |
12 | const Template: Story = (args) => ;
13 |
14 | export const BlackBrowsePageHeader = Template.bind({});
15 | BlackBrowsePageHeader.args = {
16 | onClick
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/organisms/BrowsePageHeader/BrowsePageHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Text, Button } from '@components/atoms';
4 | import { color } from '@theme/index';
5 | import { Size } from '@constants/index';
6 |
7 | interface BrowsePageHeaderProps {
8 | onClick?: () => void;
9 | }
10 |
11 | const BrowsePageHeaderWrap = styled.div`
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 | height: 10%;
16 | width: 100%;
17 | box-shadow: 0 2px 2px -2px ${color.border_primary};
18 | background-color: ${color.tertiary};
19 | z-index: 2;
20 | `;
21 |
22 | const ContentWrap = styled.div`
23 | display: flex;
24 | align-items: center;
25 | padding: 0rem 1.6rem;
26 | `;
27 |
28 | const BrowsePageHeader: React.FC = ({ onClick, ...props }) => {
29 | return (
30 |
31 |
32 |
33 | Channel browser
34 |
35 |
36 |
37 |
40 |
41 |
42 | );
43 | };
44 |
45 | export { BrowsePageHeader, BrowsePageHeaderProps };
46 |
--------------------------------------------------------------------------------
/client/src/components/organisms/ChatroomBody/ChatroomBody.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ChatroomBody, ChatroomBodyProps } from './ChatroomBody';
4 |
5 | export default {
6 | title: 'organisms/ChatroomBody',
7 | component: ChatroomBody
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackChatroomBody = Template.bind({});
13 | BlackChatroomBody.args = {
14 | title: '5주-그룹-프로젝트-슬랙b'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/organisms/ChatroomHeader/ChatroomHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { ChatroomHeader, ChatroomHeaderProps } from './ChatroomHeader';
4 |
5 | export default {
6 | title: 'organisms/ChatroomHeader',
7 | component: ChatroomHeader
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackChatroomHeader = Template.bind({});
13 | BlackChatroomHeader.args = {
14 | title: '5주-그룹-프로젝트-슬랙b'
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/organisms/CreateChannelModal/CreateChannelModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { CreateChannelModal, CreateChannelModalProps } from './CreateChannelModal';
4 |
5 | export default {
6 | title: 'organisms/CreateChannelModal',
7 | component: CreateChannelModal
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackCreateChannelModal = Template.bind({});
13 | BlackCreateChannelModal.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/organisms/Header/Header.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Header, HeaderProps } from './Header';
4 |
5 | export default {
6 | title: 'organisms/Header',
7 | component: Header
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackHeader = Template.bind({});
13 | BlackHeader.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/organisms/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { LogoImg } from '@components/atoms';
4 | import { ActiveProfileImg } from '@components/molecules';
5 | import { color } from '@theme/index';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { RootState } from '@store/reducers';
8 | import { loginAsync } from '@store/actions/user-action';
9 |
10 | interface HeaderProps {}
11 |
12 | const HeaderContainter = styled.div`
13 | display: flex;
14 | justify-content: space-between;
15 | align-items: center;
16 | padding: 0rem 1rem;
17 | min-height: 6%;
18 | background-color: ${color.primary};
19 | box-shadow: 0 1px 0 0 ${color.box_shadow_tertiary};
20 | `;
21 |
22 | const Header: React.FC = ({ ...props }) => {
23 | const { profileUri } = useSelector((state: RootState) => state.user);
24 | const dispatch = useDispatch();
25 | useEffect(() => {
26 | dispatch(loginAsync());
27 | }, []);
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export { Header, HeaderProps };
38 |
--------------------------------------------------------------------------------
/client/src/components/organisms/LoginForm/LoginForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { LoginForm, LoginFormProps } from './LoginForm';
4 |
5 | export default {
6 | title: 'organisms/LoginForm',
7 | component: LoginForm
8 | } as Meta;
9 |
10 | const Template: Story = (args) => ;
11 |
12 | export const BlackLoginForm = Template.bind({});
13 | BlackLoginForm.args = {};
14 |
--------------------------------------------------------------------------------
/client/src/components/organisms/LoginForm/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, LogoImg } from '@components/atoms';
3 | import { GithubLoginButton } from '@components/molecules';
4 | import styled from 'styled-components';
5 | import { color } from '@theme/index';
6 | import { Size } from '@constants/index';
7 |
8 | interface LoginFormProps {}
9 |
10 | const StyledLoginForm = styled.div`
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: space-between;
14 | align-items: center;
15 | height: 17rem;
16 | `;
17 |
18 | const LoginForm: React.FC = () => {
19 | return (
20 |
21 |
22 |
23 | Black에 로그인
24 |
25 |
26 | 로그인하려면 사용하는 Github 계정으로 계속해 주세요.
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export { LoginForm, LoginFormProps };
34 |
--------------------------------------------------------------------------------
/client/src/components/organisms/Sidebar/Sidebar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Story, Meta } from '@storybook/react/types-6-0';
3 | import { Channel, DM, Section } from '@components/molecules';
4 | import { Sidebar, SidebarProps } from './Sidebar';
5 |
6 | export default {
7 | title: 'organisms/Sidebar',
8 | component: Sidebar
9 | } as Meta;
10 |
11 | const mockChannelChildren = ['5주-그룹-프로젝트-슬랙', 'boost-ajae', '어몽어스'].map((item) => {
12 | return {item};
13 | });
14 |
15 | const mockDMChildren = ['J003_강동훈', 'J030_김도호', 'J211_탁성건'].map((item) => {
16 | return {item};
17 | });
18 |
19 | const mockChildren = [
20 | ,
21 |
22 | ];
23 |
24 | const Template: Story = (args) => ;
25 |
26 | export const BlackSidebar = Template.bind({});
27 | BlackSidebar.args = {
28 | children: mockChildren
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/components/organisms/ThreadBody/ThreadReplies.tsx:
--------------------------------------------------------------------------------
1 | import { Reply } from '@components/molecules';
2 | import { RootState } from '@store/reducers';
3 | import { color } from '@theme/index';
4 | import React from 'react';
5 | import { useSelector } from 'react-redux';
6 | import styled from 'styled-components';
7 |
8 | interface ThreadRepliesProps {
9 | messageId: number | null;
10 | }
11 |
12 | const ReplyBarContainer = styled.div`
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | `;
17 |
18 | const ReplyBarText = styled.p`
19 | margin: 0;
20 | color: ${color.text_primary};
21 | `;
22 |
23 | const ReplyBarLine = styled.div`
24 | height: 2px;
25 | width: 80%;
26 | background-color: ${color.quaternary};
27 | margin: 0rem 0.3rem;
28 | `;
29 |
30 | const ThreadReplies: React.FC = () => {
31 | const { replies } = useSelector((store: RootState) => store.thread);
32 |
33 | const createReplies = () => {
34 | return replies.map((reply: any) => );
35 | };
36 | const replyLength = replies.length;
37 | return (
38 | <>
39 | {replyLength !== 0 ? (
40 |
41 | {replyLength} reply
42 |
43 |
44 | ) : null}
45 | {replyLength !== 0 ? createReplies() : null}
46 | >
47 | );
48 | };
49 |
50 | export { ThreadReplies, ThreadRepliesProps };
51 |
--------------------------------------------------------------------------------
/client/src/components/organisms/ThreadHeader/ThreadHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 | import { Text } from '@components/atoms';
5 | import { HoverIcon } from '@components/molecules';
6 | import CloseIcon from '@imgs/close-icon.png';
7 | import { useHistory } from 'react-router-dom';
8 | import { uriParser } from '@utils/index';
9 | import { useSelector } from 'react-redux';
10 | import { Size } from '@constants/index';
11 |
12 | interface ThreadHeaderProps {}
13 |
14 | const ThreadHeaderContainter = styled.div`
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 | padding: 0rem 1rem;
19 | height: 10%;
20 | box-shadow: 0 3px 2px -2px ${color.border_primary};
21 | `;
22 |
23 | const TitleContainer = styled.div``;
24 |
25 | const TextWrap = styled.div`
26 | display: grid;
27 | `;
28 |
29 | const ThreadHeader: React.FC = ({ ...props }) => {
30 | const history = useHistory();
31 | const { title } = useSelector((store: any) => store.thread);
32 | const handlingCLoseButton = () => {
33 | history.push(`/client/${uriParser.getChatroomId()}`);
34 | };
35 |
36 | return (
37 |
38 |
39 |
40 | Thread
41 |
42 |
43 | {`#${title}`}
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export { ThreadHeader, ThreadHeaderProps };
52 |
--------------------------------------------------------------------------------
/client/src/components/organisms/index.ts:
--------------------------------------------------------------------------------
1 | import { Header } from './Header/Header';
2 | import { Sidebar } from './Sidebar/Sidebar';
3 | import { ChatroomHeader } from './ChatroomHeader/ChatroomHeader';
4 | import { ChatroomBody } from './ChatroomBody/ChatroomBody';
5 | import { LoginForm } from './LoginForm/LoginForm';
6 | import { BrowsePageChannel } from './BrowsePageChannel/BrowsePageChannel';
7 | import { CreateChannelModal } from './CreateChannelModal/CreateChannelModal';
8 | import { BrowsePageHeader } from './BrowsePageHeader/BrowsePageHeader';
9 | import { UserBoxModal } from './UserBoxModal/UserBoxModal';
10 | import { ThreadHeader } from './ThreadHeader/ThreadHeader';
11 | import { BrowsePageChannelList } from './BrowsePageChannelList/BrowsePageChannelList';
12 | import { ThreadBody } from './ThreadBody/ThreadBody';
13 |
14 | export {
15 | Header,
16 | Sidebar,
17 | ChatroomHeader,
18 | ChatroomBody,
19 | LoginForm,
20 | BrowsePageChannel,
21 | CreateChannelModal,
22 | BrowsePageHeader,
23 | UserBoxModal,
24 | ThreadBody,
25 | ThreadHeader,
26 | BrowsePageChannelList
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/components/templates/FlexContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledFlexContainer = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | height: 100%;
10 | width: 100%;
11 | `;
12 |
13 | const FlexContainer: React.FC = ({ children }) => {
14 | return {children};
15 | };
16 |
17 | export default FlexContainer;
18 |
--------------------------------------------------------------------------------
/client/src/components/templates/Main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledMain = styled.div`
5 | display: flex;
6 | width: 100%;
7 | height: 94%;
8 | overflow-x: auto;
9 | `;
10 |
11 | const Main: React.FC = ({ children }) => {
12 | return {children};
13 | };
14 |
15 | export default Main;
16 |
--------------------------------------------------------------------------------
/client/src/components/templates/MainBox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledMainBox = styled.main`
5 | width: -webkit-fill-available;
6 | height: 100%;
7 | `;
8 |
9 | const MainBox: React.FC = ({ children }) => {
10 | return {children};
11 | };
12 |
13 | export default MainBox;
14 |
--------------------------------------------------------------------------------
/client/src/components/templates/index.ts:
--------------------------------------------------------------------------------
1 | import FlexContainer from './FlexContainer';
2 | import Main from './Main';
3 | import MainBox from './MainBox';
4 | import Body from './Body';
5 |
6 | export { FlexContainer, Main, MainBox, Body };
7 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import store from '@store/index';
5 | import App from './shared/App';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
--------------------------------------------------------------------------------
/client/src/pages/ChannelBrowser/ChannelBrowser.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { BrowsePageChannelList, BrowsePageHeader } from '@components/organisms';
4 | import { BrowsePageControls, BrowsePageSearchBar } from '@components/molecules';
5 | import { useHistory } from 'react-router-dom';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { RootState } from '@store/reducers/index';
8 | import { initChannels } from '@store/actions/channel-action';
9 | import { createModalOpen } from '@store/actions/modal-action';
10 |
11 | interface ChannelBrowserProps {
12 | children: React.ReactNode;
13 | }
14 |
15 | const ChannelBrowserContainer = styled.div`
16 | display: flex;
17 | flex-direction: column;
18 | height: 100%;
19 | `;
20 |
21 | const SearchBarWrap = styled.div`
22 | padding: 1rem 1.5rem;
23 | `;
24 |
25 | const ChannelBrowser: React.FC = ({ children, ...props }) => {
26 | const history = useHistory();
27 |
28 | const dispatch = useDispatch();
29 | const { channelCount, channels } = useSelector((store: RootState) => store.channel);
30 | const { selectedChatroomId } = useSelector((store: RootState) => store.chatroom);
31 |
32 | const handlingCreateButtonClick = () => {
33 | dispatch(createModalOpen());
34 | };
35 |
36 | useEffect(() => {
37 | dispatch(initChannels());
38 | }, []);
39 |
40 | useEffect(() => {
41 | if (selectedChatroomId) history.push(`/client/${selectedChatroomId}`);
42 | }, [selectedChatroomId]);
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export { ChannelBrowser };
57 |
--------------------------------------------------------------------------------
/client/src/pages/Chatroom/Chatroom.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { ChatroomHeader, ChatroomBody } from '@components/organisms';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { RootState } from '@store/reducers/index';
6 | import { loadAsync } from '@store/actions/chatroom-action';
7 | import { RouteComponentProps } from 'react-router-dom';
8 |
9 | interface ChatroomProps {
10 | width?: string;
11 | }
12 |
13 | const ChatroomContainer = styled.div`
14 | height: 100%;
15 | width: ${(props) => props.width || '100%'};
16 | min-width: 35rem;
17 | `;
18 |
19 | const Chatroom: React.FC = ({ width }) => {
20 | const dispatch = useDispatch();
21 | const { selectedChatroomId, selectedChatroom, messages } = useSelector((store: RootState) => store.chatroom);
22 | const { title, users } = selectedChatroom;
23 | useEffect(() => {
24 | if (selectedChatroomId !== null) dispatch(loadAsync({ selectedChatroomId }));
25 | }, [selectedChatroomId]);
26 |
27 | return (
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export { Chatroom };
36 |
--------------------------------------------------------------------------------
/client/src/pages/Chatroom/ChatroomThread.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Chatroom, Thread } from '@pages/index';
4 | import { RouteComponentProps } from 'react-router-dom';
5 |
6 | interface ChatroomThreadProps {}
7 |
8 | const ChatroomThreadContainer = styled.div`
9 | display: flex;
10 | height: 100%;
11 | `;
12 |
13 | const ChatroomThread: React.FC = ({ ...props }) => {
14 | return (
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export { ChatroomThread };
23 |
--------------------------------------------------------------------------------
/client/src/pages/Chatroom/Thread.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { color } from '@theme/index';
4 | import { ThreadHeader, ThreadBody } from '@components/organisms';
5 | import { useDispatch } from 'react-redux';
6 | import { loadThread } from '@store/actions/thread-action';
7 | import { getThreadId } from '@utils/uriParser';
8 |
9 | interface ThreadProps {}
10 |
11 | const ThreadContainer = styled.div`
12 | width: 35%;
13 | min-width: 25rem;
14 | height: 100%;
15 | border-left: 1px solid ${color.border_primary};
16 | `;
17 |
18 | const Thread: React.FC = () => {
19 | const dispatch = useDispatch();
20 | const threadId = getThreadId();
21 |
22 | useEffect(() => {
23 | if (threadId) dispatch(loadThread({ messageId: threadId }));
24 | }, []);
25 |
26 | return (
27 |
28 |
29 | {threadId && }
30 |
31 | );
32 | };
33 |
34 | export { Thread };
35 |
--------------------------------------------------------------------------------
/client/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlexContainer } from '@components/templates';
3 | import { LoginForm } from '@components/organisms';
4 |
5 | interface LoginProps {
6 | children: React.ReactNode;
7 | }
8 |
9 | const Login: React.FC = ({ children, ...props }) => {
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export { Login };
18 |
--------------------------------------------------------------------------------
/client/src/pages/LoginLoading.tsx:
--------------------------------------------------------------------------------
1 | import { registerToken } from '@utils/index';
2 | import React, { useEffect } from 'react';
3 |
4 | interface LoginProps {}
5 |
6 | const LoginLoading: React.FC = () => {
7 | useEffect(() => {
8 | registerToken();
9 | }, []);
10 | return <>>;
11 | };
12 |
13 | export { LoginLoading };
14 |
--------------------------------------------------------------------------------
/client/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | import { Chatroom } from './Chatroom/Chatroom';
2 | import { Login } from './Login';
3 | import { ChatroomThread } from './Chatroom/ChatroomThread';
4 | import { Thread } from './Chatroom/Thread';
5 | import { LoginLoading } from './LoginLoading';
6 | import { ChannelBrowser } from './ChannelBrowser/ChannelBrowser';
7 |
8 | export { Chatroom, Login, LoginLoading, ChannelBrowser, Thread, ChatroomThread };
9 |
--------------------------------------------------------------------------------
/client/src/shared/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useEffect } from 'react';
2 | import { BrowserRouter, Switch, Route, withRouter } from 'react-router-dom';
3 | import { Chatroom, Login, LoginLoading, ChannelBrowser, ChatroomThread } from '@pages/index';
4 | import { Header, Sidebar, CreateChannelModal, UserBoxModal } from '@components/organisms';
5 | import { blockPage, uriParser } from '@utils/index';
6 | import { Main, MainBox, Body } from '@components/templates';
7 | import { ChannelModal, ProfileModal, EmojiPicker } from '@components/molecules';
8 |
9 | const App = () => {
10 | useEffect(() => {
11 | if (!uriParser.isExistParseCodeUrl()) blockPage();
12 | }, []);
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default App;
43 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "lib": ["es5", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "DOM"],
6 | "jsx": "react",
7 | "outDir": "./dist",
8 | "strict": true,
9 | "allowJs": true,
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "allowSyntheticDefaultImports": true,
13 | "moduleResolution": "node",
14 | "baseUrl": ".",
15 | "types": ["node"],
16 | "paths": {
17 | "@components/*": ["src/components/*"],
18 | "@pages/*": ["src/pages/*"],
19 | "@theme/*": ["src/common/theme/*"],
20 | "@utils/*": ["src/common/utils/*"],
21 | "@store/*": ["src/common/store/*"],
22 | "@imgs/*": ["public/imgs/*"],
23 | "@socket/*": ["src/common/socket/*"],
24 | "@constants/*": ["src/common/constants/*"]
25 | }
26 | },
27 | "exclude": ["node_modules"],
28 | "include": ["src/**/*.ts", "src/**/*.tsx", "public"]
29 | }
--------------------------------------------------------------------------------
/server/.env.sample:
--------------------------------------------------------------------------------
1 | # PORT
2 | PORT=port
3 |
4 | # DB
5 | DB_HOST=db_host
6 | DB_PORT=db_port
7 | DB_USERNAME=db_username
8 | DB_PASSWORD=db_password
9 | DB_DATABASE=db_database
10 |
11 | # REDIS
12 | REDIS_PORT=redis_port
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | env: {
4 | node: true,
5 | es2021: true,
6 | jest: true
7 | },
8 | extends: ['plugin:@typescript-eslint/eslint-recommended', 'airbnb-base', 'prettier/@typescript-eslint', 'plugin:prettier/recommended'],
9 | plugins: ['@typescript-eslint', 'prettier'],
10 | ignorePatterns: ['dist/', 'node_modules/'],
11 | rules: {
12 | 'prettier/prettier': [
13 | 'error',
14 | {
15 | endOfLine: 'auto'
16 | }
17 | ],
18 | '@typescript-eslint/no-explicit-any': 0,
19 | 'prefer-const': 0,
20 | 'import/no-unresolved': 0,
21 | 'import/extensions': 0,
22 | 'import/prefer-default-export': 0,
23 | 'class-methods-use-this': 0
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "trailingComma": "none",
6 | "bracketSpacing": true,
7 | "semi": true,
8 | "useTabs": false,
9 | "arrowParens": "always",
10 | "endOfLine": "lf"
11 | }
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # How to Start
2 | ## env
3 | > /ormconfig.env
4 | ```
5 | TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}
6 | TYPEORM_SEEDING_SEEDS=src/seeds/**/*{.ts,.js}
7 | TYPEORM_ENTITIES = src/model/**/*.ts
8 | TYPEORM_CONNECTION =
9 | TYPEORM_HOST =
10 | TYPEORM_USERNAME =
11 | TYPEORM_PASSWORD =
12 | TYPEORM_DATABASE =
13 | TYPEORM_PORT =
14 | TYPEORM_SYNCHRONIZE = true
15 | TYPEORM_LOGGING = false
16 | ```
17 | > /production.env
18 | ```
19 | # PORT
20 | PORT =
21 | # DB
22 | DB_HOST =
23 | DB_PORT =
24 | DB_USERNAME =
25 | DB_PASSWORD =
26 | DB_DATABASE =
27 | # REDIS
28 | REDIS_PORT =
29 | REDIS_HOST =
30 | # PASSPORT
31 | CLIENT_ID =
32 | CLIENT_SECRET =
33 | CALLBACK_URL =
34 | # JWT
35 | JWT_SECRET =
36 | # SESSION
37 | SESSION_SECRET =
38 | # CLIENT
39 | CLIENT_ADDRESS =
40 | ```
41 | ### DB
42 | ```
43 | docker-compose --env-file development.env up -d
44 | ```
45 | ### seed
46 | ```
47 | npm run seed
48 | ```
49 | ### start
50 | > install
51 | ```
52 | npm install
53 | ```
54 | > start
55 | ```
56 | npm run start
57 | ```
58 |
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | db:
4 | image: 'mysql'
5 | volumes:
6 | - dbdata:/var/lib/mysql
7 | environment:
8 | - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
9 | - MYSQL_DATABASE=${DB_DATABASE}
10 | - MYSQL_USER=${DB_USER}
11 | - MYSQL_PASSWORD=${DB_PASSWORD}
12 | ports:
13 | - '${DB_PORT}:3306'
14 |
15 | redis:
16 | image: 'redis'
17 | volumes:
18 | - redisdata:/data
19 | ports:
20 | - '${REDIS_PORT}:6379'
21 |
22 | volumes:
23 | dbdata:
24 | driver: local
25 | redisdata:
26 | driver: local
27 |
--------------------------------------------------------------------------------
/server/ormconfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'mysql',
3 | host: process.env.DB_HOST,
4 | port: process.env.DB_PORT,
5 | username: process.env.DB_USERNAME,
6 | password: process.env.DB_PASSWORD,
7 | database: process.env.DB_DATABASE,
8 | synchronize: true,
9 | logging: false,
10 | entities: ['src/model/**/*.ts']
11 | };
12 |
--------------------------------------------------------------------------------
/server/src/common/config/passport.ts:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import UserService from '@service/user-service';
3 | import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
4 | import { Strategy as GitHubStrategy } from 'passport-github';
5 |
6 | function passportConfig() {
7 | passport.serializeUser((user, done) => {
8 | done(null, user);
9 | });
10 |
11 | passport.deserializeUser((user, done) => {
12 | done(null, user);
13 | });
14 |
15 | passport.use(
16 | new GitHubStrategy(
17 | {
18 | clientID: process.env.CLIENT_ID,
19 | clientSecret: process.env.CLIENT_SECRET,
20 | callbackURL: process.env.CALLBACK_URL
21 | },
22 | async (accessToken, refreshToken, profile, cb) => {
23 | return cb(null, profile);
24 | }
25 | )
26 | );
27 | const opts = {
28 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
29 | secretOrKey: process.env.JWT_SECRET
30 | };
31 |
32 | passport.use(
33 | new JwtStrategy(opts, async function (jwtPayload, done) {
34 | try {
35 | const user = await UserService.getInstance().getUserById(String(jwtPayload.userId));
36 | return done(null, user);
37 | } catch (err) {
38 | return done(null, false);
39 | }
40 | })
41 | );
42 | }
43 |
44 | export default passportConfig;
45 |
--------------------------------------------------------------------------------
/server/src/common/constants/chat-type.ts:
--------------------------------------------------------------------------------
1 | const enum ChatType {
2 | DM = 'DM',
3 | Channel = 'Channel'
4 | }
5 | export default ChatType;
6 |
--------------------------------------------------------------------------------
/server/src/common/constants/default-section-name.ts:
--------------------------------------------------------------------------------
1 | const enum DefaultSectionName {
2 | Channels = 'Channels',
3 | DirectMessages = 'Direct Messages',
4 | Starred = 'Starred'
5 | }
6 |
7 | export default DefaultSectionName;
8 |
--------------------------------------------------------------------------------
/server/src/common/constants/event-name.ts:
--------------------------------------------------------------------------------
1 | const enum eventName {
2 | CREATE_MESSAGE = 'create message',
3 | UPDATE_MESSAGE = 'update message',
4 | DELETE_MESSAGE = 'delete message',
5 | CREATE_REPLY = 'create reply',
6 | UPDATE_REPLY = 'update reply',
7 | DELETE_REPLY = 'delete reply',
8 | JOIN_CHATROOM = 'join chatroom',
9 | CREATE_CHATROOM = 'create chatroom',
10 | CREATE_REACTION = 'create reaction',
11 | DELETE_REACTION = 'delete reaction',
12 | DISCONNECT = 'disconnect',
13 | JOIN_DM = 'join DM',
14 | LEAVE_CHANNEL = 'leave channel',
15 | CREATE_REPLY_REACTION = 'create reply reaction',
16 | DELETE_REPLY_REACTION = 'delete reply reaction'
17 | }
18 | export default eventName;
19 |
--------------------------------------------------------------------------------
/server/src/common/constants/http-status-code.ts:
--------------------------------------------------------------------------------
1 | const enum HttpStatusCode {
2 | OK = 200,
3 | CREATED = 201,
4 | NO_CONTENT = 204,
5 | BAD_REQUEST = 400,
6 | UNAUTHORIZED = 401,
7 | FORBIDDEN = 403,
8 | NOT_FOUND = 404,
9 | CONFLICT = 409,
10 | INTERNAL_SERVER_ERROR = 500
11 | }
12 |
13 | export default HttpStatusCode;
14 |
--------------------------------------------------------------------------------
/server/src/common/error/bad-request-error.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 |
3 | class BadRequestError extends Error {
4 | status: number;
5 |
6 | constructor() {
7 | super('Bad Request');
8 | this.status = HttpStatusCode.BAD_REQUEST;
9 | }
10 | }
11 |
12 | export default BadRequestError;
13 |
--------------------------------------------------------------------------------
/server/src/common/error/conflict-error.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 |
3 | class ConflictError extends Error {
4 | status: number;
5 |
6 | constructor() {
7 | super('Conflict');
8 | this.status = HttpStatusCode.CONFLICT;
9 | }
10 | }
11 |
12 | export default ConflictError;
13 |
--------------------------------------------------------------------------------
/server/src/common/error/forbidden-error.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 |
3 | class ForbiddenError extends Error {
4 | status: number;
5 |
6 | constructor() {
7 | super('Forbidden');
8 | this.status = HttpStatusCode.FORBIDDEN;
9 | }
10 | }
11 |
12 | export default ForbiddenError;
13 |
--------------------------------------------------------------------------------
/server/src/common/error/not-found-error.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 |
3 | class NotFoundError extends Error {
4 | status: number;
5 |
6 | constructor() {
7 | super('Not Found');
8 | this.status = HttpStatusCode.NOT_FOUND;
9 | }
10 | }
11 |
12 | export default NotFoundError;
13 |
--------------------------------------------------------------------------------
/server/src/common/error/unauthorized-error.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 |
3 | class UnauthorizedError extends Error {
4 | status: number;
5 |
6 | constructor() {
7 | super('Unauthorized');
8 | this.status = HttpStatusCode.UNAUTHORIZED;
9 | }
10 | }
11 |
12 | export default UnauthorizedError;
13 |
--------------------------------------------------------------------------------
/server/src/common/middleware/error-handler.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { Request, Response, NextFunction } from 'express';
3 |
4 | const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
5 | res.status(err.status || HttpStatusCode.INTERNAL_SERVER_ERROR).send();
6 | };
7 |
8 | export { errorHandler };
9 |
--------------------------------------------------------------------------------
/server/src/common/middleware/redis.ts:
--------------------------------------------------------------------------------
1 | import { Response, NextFunction } from 'express';
2 | import redis from 'redis';
3 | import dotenv from 'dotenv';
4 |
5 | const initEnv = () => {
6 | const envFile: string = `${process.env.NODE_ENV || 'development'}.env`;
7 | dotenv.config({ path: envFile });
8 | };
9 |
10 | class Redis {
11 | client: any;
12 |
13 | constructor() {
14 | initEnv();
15 | this.client = redis.createClient({
16 | host: process.env.REDIS_HOST,
17 | port: Number(process.env.REDIS_PORT)
18 | });
19 | }
20 |
21 | createCode = async (code, token) => {
22 | this.client.set(code, token);
23 | };
24 |
25 | deleteCode = (code) => {
26 | this.client.del(code);
27 | };
28 | }
29 |
30 | const middleware = (req: any, res: Response, next: NextFunction) => {
31 | req.Redis = new Redis();
32 | next();
33 | };
34 |
35 | export default middleware;
36 |
--------------------------------------------------------------------------------
/server/src/common/utils/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project12-B-Slack-Web/f54c18ab64ce57c0aaf097ff3b7c0991359f5e52/server/src/common/utils/index.ts
--------------------------------------------------------------------------------
/server/src/common/utils/validator.ts:
--------------------------------------------------------------------------------
1 | import { validate } from 'class-validator';
2 | import BadRequestError from '@error/bad-request-error';
3 |
4 | const validator = async (reqType: object) => {
5 | const errors = await validate(reqType);
6 | if (errors.length > 0) {
7 | throw new BadRequestError();
8 | }
9 | };
10 |
11 | export default validator;
12 |
--------------------------------------------------------------------------------
/server/src/controller/auth-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { NextFunction, Request, Response } from 'express';
3 |
4 | const AuthController = {
5 | async getUserInfo(req: Request, res: Response, next: NextFunction) {
6 | const { userId, profileUri, displayName } = req.user;
7 | try {
8 | res.status(HttpStatusCode.OK).json({ userId, profileUri, displayName });
9 | } catch (err) {
10 | next(err);
11 | }
12 | }
13 | };
14 |
15 | export default AuthController;
16 |
--------------------------------------------------------------------------------
/server/src/controller/message-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import MessageService from '@service/message-service';
3 | import { NextFunction, Request, Response } from 'express';
4 |
5 | const messageController = {
6 | async getMessages(req: Request, res: Response, next: NextFunction) {
7 | try {
8 | const { chatRoomId } = req.params;
9 | const { offsetId } = req.query;
10 | const messages = await MessageService.getInstance().getMessages(Number(chatRoomId), Number(offsetId));
11 | res.status(HttpStatusCode.OK).json(messages);
12 | } catch (err) {
13 | next(err);
14 | }
15 | },
16 | async createMessage(req: Request, res: Response, next: NextFunction) {
17 | const { userId } = req.user;
18 | const { chatRoomId, content } = req.body;
19 | try {
20 | const messageId = await MessageService.getInstance().createMessage(Number(userId), Number(chatRoomId), String(content));
21 | res.status(HttpStatusCode.CREATED).json(messageId);
22 | } catch (err) {
23 | next(err);
24 | }
25 | },
26 | async updateMessage(req: Request, res: Response, next: NextFunction) {
27 | const { content } = req.body;
28 | const { messageId } = req.params;
29 | try {
30 | await MessageService.getInstance().updateMessage(Number(messageId), String(content));
31 | res.status(HttpStatusCode.CREATED).json(messageId);
32 | } catch (err) {
33 | next(err);
34 | }
35 | },
36 | async deleteMessage(req: Request, res: Response, next: NextFunction) {
37 | const { messageId } = req.params;
38 | try {
39 | await MessageService.getInstance().deleteMessage(Number(messageId));
40 | res.status(HttpStatusCode.NO_CONTENT).send();
41 | } catch (err) {
42 | next(err);
43 | }
44 | }
45 | };
46 |
47 | export default messageController;
48 |
--------------------------------------------------------------------------------
/server/src/controller/message-reaction-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { NextFunction, Request, Response } from 'express';
3 | import MessageReactionService from '@service/message-reaction-service';
4 |
5 | const MessageReactionController = {
6 | async createMessageReaction(req: Request, res: Response, next: NextFunction) {
7 | const { userId } = req.user;
8 | const { messageId, title, emoji } = req.body;
9 | try {
10 | const messageReaction = await MessageReactionService.getInstance().createMessageReaction(Number(userId), Number(messageId), title, emoji);
11 | res.status(HttpStatusCode.CREATED).json({ messageReaction });
12 | } catch (err) {
13 | next(err);
14 | }
15 | },
16 | async deleteMessageReaction(req: Request, res: Response, next: NextFunction) {
17 | const { userId } = req.user;
18 | const { messageId, reactionId } = req.params;
19 | try {
20 | await MessageReactionService.getInstance().deleteMessageReaction(Number(userId), Number(messageId), Number(reactionId));
21 | res.status(HttpStatusCode.NO_CONTENT).send();
22 | } catch (err) {
23 | next(err);
24 | }
25 | }
26 | };
27 |
28 | export default MessageReactionController;
29 |
--------------------------------------------------------------------------------
/server/src/controller/oauth-controller.ts:
--------------------------------------------------------------------------------
1 | import UserService from '@service/user-service';
2 | import { Request, Response, NextFunction } from 'express';
3 | import jwt from 'jsonwebtoken';
4 | import randomstring from 'randomstring';
5 |
6 | interface user {
7 | userId: number;
8 | username: string;
9 | profileUri: string;
10 | displayName: string;
11 | }
12 |
13 | declare module 'express' {
14 | interface Request {
15 | user?: user;
16 | }
17 | }
18 |
19 | const OauthController = {
20 | async OauthCallback(req: any, res: Response) {
21 | const { username, photos } = req.user;
22 | const profileUri = photos && photos[0].value;
23 | try {
24 | await UserService.getInstance().getUserById(username);
25 | } catch {
26 | await UserService.getInstance().createUser(username, profileUri);
27 | }
28 | const token = jwt.sign(
29 | {
30 | userId: req.user.username
31 | },
32 | process.env.JWT_SECRET,
33 | { expiresIn: '1h' }
34 | );
35 | const randomValue = randomstring.generate(7);
36 | req.Redis.createCode(randomValue, token);
37 |
38 | res.redirect(`${process.env.CLIENT_ADDRESS}?code=${randomValue}`);
39 | },
40 |
41 | async getToken(req: any, res: Response, next: NextFunction) {
42 | try {
43 | const { code } = req.params;
44 | req.Redis.client.get(code, (err, Token) => {
45 | res.setHeader('Access-Control-Expose-Headers', 'Authorization');
46 | res.setHeader('Authorization', `Bearer ${Token}`);
47 | res.end();
48 | });
49 | } catch (err) {
50 | next(err);
51 | }
52 | }
53 | };
54 |
55 | export default OauthController;
56 |
--------------------------------------------------------------------------------
/server/src/controller/reaction-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { NextFunction, Request, Response } from 'express';
3 | import ReactionService from '@service/reaction-service';
4 |
5 | const ReactionController = {
6 | async createReaction(req: Request, res: Response, next: NextFunction) {
7 | const { title, emoji } = req.body;
8 | try {
9 | const reactionId = await ReactionService.getInstance().createReaction(title, emoji);
10 | res.status(HttpStatusCode.CREATED).json({ reactionId });
11 | } catch (err) {
12 | next(err);
13 | }
14 | },
15 | async getReactions(req: Request, res: Response, next: NextFunction) {
16 | try {
17 | const reactions = await ReactionService.getInstance().getReactions();
18 | res.status(HttpStatusCode.OK).json(reactions);
19 | } catch (err) {
20 | next(err);
21 | }
22 | }
23 | };
24 |
25 | export default ReactionController;
26 |
--------------------------------------------------------------------------------
/server/src/controller/reply-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { NextFunction, Request, Response } from 'express';
3 | import ReplyService from '@service/reply-service';
4 | import messageService from '@service/message-service';
5 |
6 | const ReplyController = {
7 | async createReply(req: Request, res: Response, next: NextFunction) {
8 | const { userId } = req.user;
9 | const { messageId, content } = req.body;
10 | try {
11 | const replyId = await ReplyService.getInstance().createReply(Number(userId), Number(messageId), content);
12 | res.status(HttpStatusCode.CREATED).json({ replyId });
13 | } catch (err) {
14 | next(err);
15 | }
16 | },
17 | async getReply(req: Request, res: Response, next: NextFunction) {
18 | const { replyId } = req.params;
19 | try {
20 | const reply = await ReplyService.getInstance().getReply(Number(replyId));
21 | res.status(HttpStatusCode.OK).json(reply);
22 | } catch (err) {
23 | next(err);
24 | }
25 | },
26 | async getReplies(req: Request, res: Response, next: NextFunction) {
27 | const { offsetId } = req.query;
28 | const { messageId } = req.params;
29 | try {
30 | const message = await messageService.getInstance().getMessage(Number(messageId));
31 | const replies = await ReplyService.getInstance().getReplies(Number(messageId), Number(offsetId));
32 | const replyCount = await ReplyService.getInstance().getReplyCount(Number(messageId));
33 | res.header('Access-Control-Expose-Headers', 'x-total-count');
34 | res.setHeader('x-total-count', replyCount);
35 | res.status(HttpStatusCode.OK).json({ message, replies });
36 | } catch (err) {
37 | next(err);
38 | }
39 | }
40 | };
41 |
42 | export default ReplyController;
43 |
--------------------------------------------------------------------------------
/server/src/controller/user-chatroom-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import { NextFunction, Request, Response } from 'express';
3 | import UserChatroomService from '@service/user-chatroom-service';
4 |
5 | const UserChatroomController = {
6 | async joinChatroom(req: Request, res: Response, next: NextFunction) {
7 | const { userId } = req.user;
8 | const { chatroomId } = req.body;
9 | try {
10 | await UserChatroomService.getInstance().joinChatroom(Number(userId), Number(chatroomId));
11 | res.status(HttpStatusCode.CREATED).send();
12 | } catch (err) {
13 | next(err);
14 | }
15 | },
16 | async inviteChatroom(req: Request, res: Response, next: NextFunction) {
17 | const { users, chatroomId } = req.body;
18 | try {
19 | await UserChatroomService.getInstance().inviteChatroom(users, Number(chatroomId));
20 | res.status(HttpStatusCode.CREATED).send();
21 | } catch (err) {
22 | next(err);
23 | }
24 | },
25 | async getUserChatrooms(req: Request, res: Response, next: NextFunction) {
26 | const { userId } = req.user;
27 | try {
28 | const users = await UserChatroomService.getInstance().getUserChatrooms(userId);
29 | res.status(HttpStatusCode.OK).json(users);
30 | } catch (err) {
31 | next(err);
32 | }
33 | }
34 | };
35 |
36 | export default UserChatroomController;
37 |
--------------------------------------------------------------------------------
/server/src/controller/user-controller.ts:
--------------------------------------------------------------------------------
1 | import HttpStatusCode from '@constants/http-status-code';
2 | import UserService from '@service/user-service';
3 | import { NextFunction, Request, Response } from 'express';
4 |
5 | const UserController = {
6 | async getUsers(req: Request, res: Response, next: NextFunction) {
7 | try {
8 | const users = await UserService.getInstance().getUsers();
9 | res.status(HttpStatusCode.OK).json(users);
10 | } catch (err) {
11 | next(err);
12 | }
13 | },
14 | async getUser(req: Request, res: Response, next: NextFunction) {
15 | const { userId } = req.params;
16 | try {
17 | const user = await UserService.getInstance().getUser(Number(userId));
18 | res.status(HttpStatusCode.OK).json(user);
19 | } catch (err) {
20 | next(err);
21 | }
22 | }
23 | };
24 |
25 | export default UserController;
26 |
--------------------------------------------------------------------------------
/server/src/model/chatroom.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm';
2 | import { IsBoolean, IsIn, IsString } from 'class-validator';
3 | import UserChatroom from '@model/user-chatroom';
4 | import Message from '@model/message';
5 | import ChatType from '@constants/chat-type';
6 |
7 | @Entity({ name: 'chatroom' })
8 | export default class Chatroom {
9 | @PrimaryGeneratedColumn()
10 | chatroomId: number;
11 |
12 | @Column({ unique: true, nullable: true })
13 | @IsString()
14 | title: string;
15 |
16 | @Column({ nullable: true })
17 | description: string;
18 |
19 | @Column()
20 | @IsBoolean()
21 | isPrivate: boolean;
22 |
23 | @Column()
24 | @IsIn([ChatType.DM, ChatType.Channel])
25 | chatType: string;
26 |
27 | @Column({ nullable: true })
28 | topic: string;
29 |
30 | @CreateDateColumn()
31 | createdAt: Date;
32 |
33 | @UpdateDateColumn()
34 | updatedAt: Date;
35 |
36 | @DeleteDateColumn()
37 | deletedAt: Date;
38 |
39 | @OneToMany(() => UserChatroom, (userChatroom) => userChatroom.chatroom)
40 | userChatrooms: UserChatroom[];
41 |
42 | @OneToMany(() => Message, (message) => message.chatroom)
43 | messages: Message[];
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/model/message-reaction.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm';
2 | import Reaction from '@model/reaction';
3 | import User from '@model/user';
4 | import Message from '@model/message';
5 |
6 | @Entity({ name: 'message_reaction' })
7 | export default class MessageReaction {
8 | @PrimaryGeneratedColumn()
9 | messageReactionId: number;
10 |
11 | @CreateDateColumn()
12 | createdAt: Date;
13 |
14 | @UpdateDateColumn()
15 | updatedAt: Date;
16 |
17 | @DeleteDateColumn()
18 | deletedAt: Date;
19 |
20 | @ManyToOne(() => Reaction, (reaction) => reaction.reactionId)
21 | @JoinColumn({ name: 'reactionId' })
22 | reaction: Reaction;
23 |
24 | @ManyToOne(() => User, (user) => user.userId)
25 | @JoinColumn({ name: 'userId' })
26 | user: User;
27 |
28 | @ManyToOne(() => Message, (message) => message.messageId)
29 | @JoinColumn({ name: 'messageId' })
30 | message: Message;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/model/message.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryGeneratedColumn,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | ManyToOne,
8 | OneToMany,
9 | JoinColumn,
10 | DeleteDateColumn
11 | } from 'typeorm';
12 | import User from '@model/user';
13 | import Chatroom from '@model/chatroom';
14 | import MessageReaction from '@model/message-reaction';
15 | import Reply from '@model/reply';
16 |
17 | @Entity({ name: 'message' })
18 | export default class Message {
19 | @PrimaryGeneratedColumn()
20 | messageId: number;
21 |
22 | @Column()
23 | content: string;
24 |
25 | @CreateDateColumn()
26 | createdAt: Date;
27 |
28 | @UpdateDateColumn()
29 | updatedAt: Date;
30 |
31 | @DeleteDateColumn()
32 | deletedAt: Date;
33 |
34 | @ManyToOne(() => User, (user) => user.userId)
35 | @JoinColumn({ name: 'userId' })
36 | user: User;
37 |
38 | @ManyToOne(() => Chatroom, (chatroom) => chatroom.chatroomId)
39 | @JoinColumn({ name: 'chatroomId' })
40 | chatroom: Chatroom;
41 |
42 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.message)
43 | messageReactions: MessageReaction[];
44 |
45 | @OneToMany(() => Reply, (reply) => reply.message)
46 | replies: Reply[];
47 | }
48 |
--------------------------------------------------------------------------------
/server/src/model/reaction.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm';
2 | import MessageReaction from '@model/message-reaction';
3 | import ReplyReaction from '@model/reply-reaction';
4 | import { IsString } from 'class-validator';
5 |
6 | @Entity({ name: 'reaction' })
7 | export default class Reaction {
8 | @PrimaryGeneratedColumn()
9 | reactionId: number;
10 |
11 | @Column({ length: 100, unique: true })
12 | @IsString()
13 | title: string;
14 |
15 | @Column({ length: 10 })
16 | @IsString()
17 | emoji: string;
18 |
19 | @CreateDateColumn()
20 | createdAt: Date;
21 |
22 | @UpdateDateColumn()
23 | updatedAt: Date;
24 |
25 | @DeleteDateColumn()
26 | deletedAt: Date;
27 |
28 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.reaction)
29 | messageReactions: MessageReaction[];
30 |
31 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.reaction)
32 | replyReactions: ReplyReaction[];
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/model/reply-reaction.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm';
2 | import Reaction from '@model/reaction';
3 | import User from '@model/user';
4 | import Reply from '@model/reply';
5 |
6 | @Entity({ name: 'reply_reaction' })
7 | export default class ReplyReaction {
8 | @PrimaryGeneratedColumn()
9 | replyReactionId: number;
10 |
11 | @CreateDateColumn()
12 | createdAt: Date;
13 |
14 | @UpdateDateColumn()
15 | updatedAt: Date;
16 |
17 | @DeleteDateColumn()
18 | deletedAt: Date;
19 |
20 | @ManyToOne(() => Reaction, (reaction) => reaction.reactionId)
21 | @JoinColumn({ name: 'reactionId' })
22 | reaction: Reaction;
23 |
24 | @ManyToOne(() => User, (user) => user.userId)
25 | @JoinColumn({ name: 'userId' })
26 | user: User;
27 |
28 | @ManyToOne(() => Reply, (reply) => reply.replyId)
29 | @JoinColumn({ name: 'replyId' })
30 | reply: Reply;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/model/reply.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryGeneratedColumn,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | ManyToOne,
8 | JoinColumn,
9 | DeleteDateColumn,
10 | OneToMany
11 | } from 'typeorm';
12 | import User from '@model/user';
13 | import Message from '@model/message';
14 | import ReplyReaction from '@model/reply-reaction';
15 | import { IsString } from 'class-validator';
16 |
17 | @Entity({ name: 'reply' })
18 | export default class Reply {
19 | @PrimaryGeneratedColumn()
20 | replyId: number;
21 |
22 | @Column()
23 | @IsString()
24 | content: string;
25 |
26 | @CreateDateColumn()
27 | createdAt: Date;
28 |
29 | @UpdateDateColumn()
30 | updatedAt: Date;
31 |
32 | @DeleteDateColumn()
33 | deletedAt: Date;
34 |
35 | @ManyToOne(() => User, (user) => user.userId)
36 | @JoinColumn({ name: 'userId' })
37 | user: User;
38 |
39 | @ManyToOne(() => Message, (message) => message.messageId)
40 | @JoinColumn({ name: 'messageId' })
41 | message: Message;
42 |
43 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.reply)
44 | replyReactions: ReplyReaction[];
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/model/section.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm';
2 | import User from '@model/user';
3 |
4 | @Entity({ name: 'section' })
5 | export default class Section {
6 | @PrimaryGeneratedColumn()
7 | sectionId: number;
8 |
9 | @Column()
10 | sectionName: string;
11 |
12 | @CreateDateColumn()
13 | createdAt: Date;
14 |
15 | @UpdateDateColumn()
16 | updatedAt: Date;
17 |
18 | @DeleteDateColumn()
19 | deletedAt: Date;
20 |
21 | @ManyToOne(() => User, (user) => user.userId)
22 | @JoinColumn({ name: 'userId' })
23 | user: User;
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/model/socket.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
2 | import User from '@model/user';
3 |
4 | @Entity({ name: 'socket' })
5 | export default class Socket {
6 | @PrimaryGeneratedColumn()
7 | id: number;
8 |
9 | @ManyToOne(() => User, (user) => user.userId)
10 | @JoinColumn({ name: 'userId' })
11 | user: User;
12 |
13 | @Column()
14 | socketId: string;
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/model/user-chatroom.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm';
2 | import User from '@model/user';
3 | import Chatroom from '@model/chatroom';
4 |
5 | @Entity({ name: 'user_chatroom' })
6 | export default class UserChatroom {
7 | @PrimaryGeneratedColumn()
8 | userChatroomId: number;
9 |
10 | @Column()
11 | sectionName: string;
12 |
13 | @CreateDateColumn()
14 | createdAt: Date;
15 |
16 | @UpdateDateColumn()
17 | updatedAt: Date;
18 |
19 | @DeleteDateColumn()
20 | deletedAt: Date;
21 |
22 | @ManyToOne(() => User, (user) => user.userId)
23 | @JoinColumn({ name: 'userId' })
24 | user: User;
25 |
26 | @ManyToOne(() => Chatroom, (chatroom) => chatroom.chatroomId)
27 | @JoinColumn({ name: 'chatroomId' })
28 | chatroom: Chatroom;
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/model/user.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm';
2 | import { IsUrl } from 'class-validator';
3 | import Section from '@model/section';
4 | import UserChatroom from '@model/user-chatroom';
5 | import Message from '@model/message';
6 | import MessageReaction from '@model/message-reaction';
7 | import Reply from '@model/reply';
8 | import ReplyReaction from '@model/reply-reaction';
9 | import Socket from '@model/socket';
10 |
11 | @Entity({ name: 'user' })
12 | export default class User {
13 | @PrimaryGeneratedColumn()
14 | userId: number;
15 |
16 | @Column({ length: 30, unique: true })
17 | id: string;
18 |
19 | @Column({ length: 40, nullable: true })
20 | password: string;
21 |
22 | @Column({ nullable: true })
23 | @IsUrl()
24 | profileUri: string;
25 |
26 | @Column({ length: 20 })
27 | fullName: string;
28 |
29 | @Column({ length: 20 })
30 | displayName: string;
31 |
32 | @Column({ nullable: true })
33 | whatIDo: string;
34 |
35 | @Column({ length: 20, nullable: true })
36 | phoneNumber: string;
37 |
38 | @Column()
39 | isSocial: boolean;
40 |
41 | @CreateDateColumn()
42 | createdAt: Date;
43 |
44 | @UpdateDateColumn()
45 | updatedAt: Date;
46 |
47 | @DeleteDateColumn()
48 | deletedAt: Date;
49 |
50 | @OneToMany(() => Section, (section) => section.user)
51 | sections: Section[];
52 |
53 | @OneToMany(() => UserChatroom, (userChatroom) => userChatroom.user)
54 | userChatrooms: UserChatroom[];
55 |
56 | @OneToMany(() => Message, (message) => message.user)
57 | messages: Message[];
58 |
59 | @OneToMany(() => MessageReaction, (messageReaction) => messageReaction.user)
60 | messageReactions: MessageReaction[];
61 |
62 | @OneToMany(() => Reply, (reply) => reply.user)
63 | replies: Reply[];
64 |
65 | @OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.user)
66 | replyReactions: ReplyReaction[];
67 |
68 | @OneToMany(() => Socket, (socket) => socket.user)
69 | sockets: Socket[];
70 | }
71 |
--------------------------------------------------------------------------------
/server/src/repository/chatroom-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Chatroom from '@model/chatroom';
3 |
4 | @EntityRepository(Chatroom)
5 | export default class ChatroomRepository extends Repository {
6 | findByTitle(title: string) {
7 | return this.findOne({ title });
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/repository/message-reaction-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import MessageReaction from '@model/message-reaction';
3 |
4 | @EntityRepository(MessageReaction)
5 | export default class MessageReactionRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/message-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Message from '@model/message';
3 |
4 | @EntityRepository(Message)
5 | export default class MessageRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/reacion-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Reaction from '@model/reaction';
3 |
4 | @EntityRepository(Reaction)
5 | export default class ReactionRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/reply-reaction-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import ReplyReaction from '@model/reply-reaction';
3 |
4 | @EntityRepository(ReplyReaction)
5 | export default class ReplyReactionRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/reply-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Reply from '@model/reply';
3 |
4 | @EntityRepository(Reply)
5 | export default class ReplyRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/section-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Section from '@model/section';
3 |
4 | @EntityRepository(Section)
5 | export default class SectionRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/socket-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import Socket from '@model/socket';
3 |
4 | @EntityRepository(Socket)
5 | export default class SocketRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/user-chatroom-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import UserChatroom from '@model/user-chatroom';
3 |
4 | @EntityRepository(UserChatroom)
5 | export default class UserChatroomRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/repository/user-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import User from '@model/user';
3 |
4 | @EntityRepository(User)
5 | export default class UserRepository extends Repository {}
6 |
--------------------------------------------------------------------------------
/server/src/router/api.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import userRouter from '@router/user-router';
3 | import messageRouter from '@router/message-router';
4 | import chatroomRouter from '@router/chatroom-router';
5 | import userChatroomRouter from '@router/user-chatroom-router';
6 | import replyRouter from '@router/reply-router';
7 | import reactionRouter from '@router/reaction-router';
8 | import authRouter from '@router/auth-router';
9 | import messageReactionRouter from '@router/message-reaction-router';
10 |
11 | const router = express.Router();
12 |
13 | router.use('/messages', messageRouter);
14 | router.use('/users', userRouter);
15 | router.use('/chatrooms', chatroomRouter);
16 | router.use('/user-chatrooms', userChatroomRouter);
17 | router.use('/replies', replyRouter);
18 | router.use('/reactions', reactionRouter);
19 | router.use('/auth', authRouter);
20 | router.use('/message-reactions', messageReactionRouter);
21 |
22 | export default router;
23 |
--------------------------------------------------------------------------------
/server/src/router/auth-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import AuthController from '@controller/auth-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/', AuthController.getUserInfo);
7 |
8 | export default router;
9 |
--------------------------------------------------------------------------------
/server/src/router/chatroom-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import ChatroomController from '@controller/chatroom-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/', ChatroomController.getChatrooms);
7 | router.post('/channel', ChatroomController.createChannel);
8 | router.post('/dm', ChatroomController.createDM);
9 | router.get('/:chatroomId', ChatroomController.getChatroomInfo);
10 | router.patch('/:chatroomId', ChatroomController.updateChatroom);
11 | router.delete('/:chatroomId', ChatroomController.deleteChatroom);
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/server/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import passport from 'passport';
3 | import oauthRouter from '@router/oauth';
4 | import apiRouter from '@router/api';
5 |
6 | const router = express.Router();
7 |
8 | router.use('/oauth', oauthRouter);
9 | router.use('/api', passport.authenticate('jwt', { session: false }), apiRouter);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/server/src/router/message-reaction-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import MessageReactionController from '@controller/message-reaction-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.post('/', MessageReactionController.createMessageReaction);
7 | router.delete('/:messageId/:reactionId', MessageReactionController.deleteMessageReaction);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/server/src/router/message-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import messageController from '@controller/message-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/:chatRoomId', messageController.getMessages);
7 | router.post('/', messageController.createMessage);
8 | router.patch('/:messageId', messageController.updateMessage);
9 | router.delete('/:messageId', messageController.deleteMessage);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/server/src/router/oauth.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import OauthController from '@controller/oauth-controller';
3 | import passport from 'passport';
4 |
5 | const router = express.Router();
6 |
7 | router.get('/github/login', passport.authenticate('github'));
8 | router.get('/github/callback', passport.authenticate('github', { failureRedirect: '/' }), OauthController.OauthCallback);
9 | router.get('/github/:code', OauthController.getToken);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/server/src/router/reaction-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import ReactionController from '@controller/reaction-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.post('/', ReactionController.createReaction);
7 | router.get('/', ReactionController.getReactions);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/server/src/router/reply-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import ReplyController from '@controller/reply-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.post('/', ReplyController.createReply);
7 | router.get('/', ReplyController.getReplies);
8 | router.get('/:messageId', ReplyController.getReplies);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/server/src/router/user-chatroom-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import UserChatroomController from '@controller/user-chatroom-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.post('/', UserChatroomController.joinChatroom);
7 | router.post('/others', UserChatroomController.inviteChatroom);
8 | router.get('/', UserChatroomController.getUserChatrooms);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/server/src/router/user-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import UserController from '@controller/user-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/', UserController.getUsers);
7 | router.get('/:userId', UserController.getUser);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/server/src/seeds/chatroom.ts:
--------------------------------------------------------------------------------
1 | import { Factory, Seeder } from 'typeorm-seeding';
2 | import { Connection } from 'typeorm';
3 | import ChatType from '@constants/chat-type';
4 | import Chatroom from '@model/chatroom';
5 |
6 | export default class CreateChatrooms implements Seeder {
7 | public async run(factory: Factory, connection: Connection): Promise {
8 | const chatroomData = [
9 | {
10 | title: 'random',
11 | isPrivate: false,
12 | chatType: ChatType.Channel
13 | }
14 | ];
15 | const res = await connection.getRepository(Chatroom).findOne(chatroomData[0]);
16 | if (!res) await connection.getRepository(Chatroom).save(chatroomData);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/service/reaction-service.ts:
--------------------------------------------------------------------------------
1 | import { getCustomRepository } from 'typeorm';
2 | import ReactionRepository from '@repository/reacion-repository';
3 | import validator from '@utils/validator';
4 | import BadRequestError from '@error/bad-request-error';
5 |
6 | class ReactionService {
7 | static instance: ReactionService;
8 |
9 | reactionRepository: ReactionRepository;
10 |
11 | constructor() {
12 | this.reactionRepository = getCustomRepository(ReactionRepository);
13 | }
14 |
15 | static getInstance(): ReactionService {
16 | if (!ReactionService.instance) {
17 | ReactionService.instance = new ReactionService();
18 | }
19 | return ReactionService.instance;
20 | }
21 |
22 | async createReaction(title: string, emoji: string) {
23 | const reaction = await this.reactionRepository.findOne({ title });
24 | if (reaction) {
25 | throw new BadRequestError();
26 | }
27 |
28 | const newReaction = this.reactionRepository.create({ title, emoji });
29 | await validator(newReaction);
30 | const createdReaction = await this.reactionRepository.save(newReaction);
31 | return createdReaction.reactionId;
32 | }
33 |
34 | async getReactions() {
35 | const reactions = await this.reactionRepository
36 | .createQueryBuilder('reaction')
37 | .select(['reaction.reactionId', 'reaction.title', 'reaction.emoji'])
38 | .getMany();
39 |
40 | return reactions;
41 | }
42 | }
43 |
44 | export default ReactionService;
45 |
--------------------------------------------------------------------------------
/server/src/service/socket-service.ts:
--------------------------------------------------------------------------------
1 | import { getCustomRepository } from 'typeorm';
2 | import SocketRepository from '@repository/socket-repository';
3 | import UserRepository from '@repository/user-repository';
4 | import validator from '@utils/validator';
5 |
6 | class SocketService {
7 | static instance: SocketService;
8 |
9 | socketRepository: SocketRepository;
10 |
11 | userRepository: UserRepository;
12 |
13 | constructor() {
14 | this.socketRepository = getCustomRepository(SocketRepository);
15 | this.userRepository = getCustomRepository(UserRepository);
16 | }
17 |
18 | static getInstance(): SocketService {
19 | if (!SocketService.instance) {
20 | SocketService.instance = new SocketService();
21 | }
22 | return SocketService.instance;
23 | }
24 |
25 | async createSocket(userId, socketId) {
26 | const user = await this.userRepository.findOne({ userId });
27 | const newSocket = this.socketRepository.create({ user, socketId });
28 | await this.socketRepository.save(newSocket);
29 | }
30 |
31 | async deleteSocket(socketId) {
32 | await this.socketRepository.delete({ socketId });
33 | }
34 |
35 | async getSocketId(userId) {
36 | const user = await this.userRepository.findOne({ userId });
37 | const socketInfos = await this.socketRepository.find({ user });
38 | return socketInfos;
39 | }
40 | }
41 |
42 | export default SocketService;
43 |
--------------------------------------------------------------------------------
/server/src/socket/event/chatroom-event.ts:
--------------------------------------------------------------------------------
1 | import chatroomHandler from '@socket/handler/chatroom-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const chatroomEvent = (io, socket) => {
5 | socket.on(eventName.JOIN_CHATROOM, (chatroom) => chatroomHandler.joinChatroom(io, socket, chatroom));
6 | socket.on(eventName.JOIN_DM, (DirectMessage) => chatroomHandler.joinDM(io, socket, DirectMessage));
7 | socket.on(eventName.LEAVE_CHANNEL, (chatroom) => chatroomHandler.leaveChatroom(io, socket, chatroom));
8 | };
9 |
10 | export default chatroomEvent;
11 |
--------------------------------------------------------------------------------
/server/src/socket/event/connection-event.ts:
--------------------------------------------------------------------------------
1 | import chatroomHandler from '@socket/handler/chatroom-handler';
2 | import socketHandler from '@socket/handler/socket-handler';
3 |
4 | const connectionEvent = (io, socket) => {
5 | chatroomHandler.initJoinChatroom(io, socket);
6 | socketHandler.saveSocketId(io, socket);
7 | };
8 |
9 | export default connectionEvent;
10 |
--------------------------------------------------------------------------------
/server/src/socket/event/disconnect-event.ts:
--------------------------------------------------------------------------------
1 | import socketHandler from '@socket/handler/socket-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageEvent = (io, socket) => {
5 | socket.on(eventName.DISCONNECT, () => socketHandler.deleteSocketId(io, socket));
6 | };
7 |
8 | export default messageEvent;
9 |
--------------------------------------------------------------------------------
/server/src/socket/event/message-event.ts:
--------------------------------------------------------------------------------
1 | import messageHandler from '@socket/handler/message-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageEvent = (io, socket) => {
5 | socket.on(eventName.CREATE_MESSAGE, (message) => messageHandler.createMessage(io, socket, message));
6 | socket.on(eventName.UPDATE_MESSAGE, (message) => messageHandler.updateMessage(io, socket, message));
7 | socket.on(eventName.DELETE_MESSAGE, (message) => messageHandler.deleteMessage(io, socket, message));
8 | };
9 |
10 | export default messageEvent;
11 |
--------------------------------------------------------------------------------
/server/src/socket/event/message-reaction-event.ts:
--------------------------------------------------------------------------------
1 | import messageReactionHandler from '@socket/handler/message-reaction-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageReactionEvent = (io, socket) => {
5 | socket.on(eventName.CREATE_REACTION, (messageReaction) => messageReactionHandler.createMessageReaction(io, socket, messageReaction));
6 | socket.on(eventName.DELETE_REACTION, (messageReaction) => messageReactionHandler.deleteMessageReaction(io, socket, messageReaction));
7 | };
8 |
9 | export default messageReactionEvent;
10 |
--------------------------------------------------------------------------------
/server/src/socket/event/reply-event.ts:
--------------------------------------------------------------------------------
1 | import replyHandler from '@socket/handler/reply-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageEvent = (io, socket) => {
5 | socket.on(eventName.CREATE_REPLY, (reply) => replyHandler.createReply(io, socket, reply));
6 | socket.on(eventName.UPDATE_REPLY, (reply) => replyHandler.updateReply(io, socket, reply));
7 | socket.on(eventName.DELETE_REPLY, (reply) => replyHandler.deleteReply(io, socket, reply));
8 | };
9 |
10 | export default messageEvent;
11 |
--------------------------------------------------------------------------------
/server/src/socket/event/reply-rection-event.ts:
--------------------------------------------------------------------------------
1 | import replyReactionHandler from '@socket/handler/reply-reaction-handler';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageReactionEvent = (io, socket) => {
5 | socket.on(eventName.CREATE_REPLY_REACTION, (replyReaction) => replyReactionHandler.createReplyReaction(io, socket, replyReaction));
6 | socket.on(eventName.DELETE_REPLY_REACTION, (replyReaction) => replyReactionHandler.deleteReplytReaction(io, socket, replyReaction));
7 | };
8 |
9 | export default messageReactionEvent;
10 |
--------------------------------------------------------------------------------
/server/src/socket/handler/message-handler.ts:
--------------------------------------------------------------------------------
1 | import MessageService from '@service/message-service';
2 | import eventName from '@constants/event-name';
3 |
4 | const messageHandler = {
5 | async createMessage(io, socket, message) {
6 | const req = socket.request;
7 | const { chatroomId, content } = message;
8 | const { userId } = req.user;
9 | const messageId = await MessageService.getInstance().createMessage(userId, chatroomId, content);
10 | const newMessage = await MessageService.getInstance().getMessage(messageId);
11 | io.to(String(chatroomId)).emit(eventName.CREATE_MESSAGE, { ...newMessage, chatroomId });
12 | },
13 | async updateMessage(io, socket, message) {
14 | const { messageId, content } = message;
15 | const messageInfo = await MessageService.getInstance().getMessage(messageId);
16 | const { chatroomId } = messageInfo.chatroom;
17 | const updateMessage = await MessageService.getInstance().updateMessage(messageId, content);
18 | io.to(String(chatroomId)).emit(eventName.UPDATE_MESSAGE, updateMessage);
19 | },
20 | async deleteMessage(io, socket, message) {
21 | const { messageId } = message;
22 | const messageInfo = await MessageService.getInstance().getMessage(messageId);
23 | const { chatroomId } = messageInfo.chatroom;
24 | await MessageService.getInstance().deleteMessage(messageId);
25 | io.to(String(chatroomId)).emit(eventName.DELETE_MESSAGE, { messageId });
26 | }
27 | };
28 |
29 | export default messageHandler;
30 |
--------------------------------------------------------------------------------
/server/src/socket/handler/message-reaction-handler.ts:
--------------------------------------------------------------------------------
1 | import MessageReactionService from '@service/message-reaction-service';
2 | import MessageServie from '@service/message-service';
3 | import eventName from '@constants/event-name';
4 |
5 | const messageHandler = {
6 | async createMessageReaction(io, socket, messageReaction) {
7 | const req = socket.request;
8 | const { userId } = req.user;
9 | const { messageId, title, emoji } = messageReaction;
10 | const newMessageReaction = await MessageReactionService.getInstance().createMessageReaction(Number(userId), Number(messageId), title, emoji);
11 | const { chatroomId } = (await MessageServie.getInstance().getMessage(messageId)).chatroom;
12 | io.to(String(chatroomId)).emit(eventName.CREATE_REACTION, { ...newMessageReaction, chatroomId });
13 | },
14 |
15 | async deleteMessageReaction(io, socket, messageReaction) {
16 | const req = socket.request;
17 | const { userId } = req.user;
18 | const { messageId, reactionId } = messageReaction;
19 | await MessageReactionService.getInstance().deleteMessageReaction(Number(userId), Number(messageId), Number(reactionId));
20 | const newMessageReaction = await MessageReactionService.getInstance().getMessageReaction(Number(messageId), Number(reactionId));
21 | const { chatroomId } = (await MessageServie.getInstance().getMessage(messageId)).chatroom;
22 | io.to(String(chatroomId)).emit(eventName.DELETE_REACTION, { ...newMessageReaction, chatroomId });
23 | }
24 | };
25 |
26 | export default messageHandler;
27 |
--------------------------------------------------------------------------------
/server/src/socket/handler/reply-handler.ts:
--------------------------------------------------------------------------------
1 | import ReplyService from '@service/reply-service';
2 | import eventName from '@constants/event-name';
3 |
4 | const replyHandler = {
5 | async createReply(io, socket, reply) {
6 | const req = socket.request;
7 | const { userId } = req.user;
8 | const { messageId, content } = reply;
9 | const replyId = await ReplyService.getInstance().createReply(userId, messageId, content);
10 | const newReply = await ReplyService.getInstance().getReply(replyId);
11 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId);
12 | const { chatroomId } = replyInfo.message.chatroom;
13 | io.to(String(chatroomId)).emit(eventName.CREATE_REPLY, { ...newReply, chatroomId, messageId });
14 | },
15 |
16 | async updateReply(io, socket, reply) {
17 | const { replyId, content, messageId } = reply;
18 | await ReplyService.getInstance().updateReply(replyId, content);
19 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId);
20 | const { chatroomId } = replyInfo.message.chatroom;
21 | io.to(String(chatroomId)).emit(eventName.UPDATE_REPLY, { replyId, content, chatroomId, messageId });
22 | },
23 |
24 | async deleteReply(io, socket, reply) {
25 | const { replyId } = reply;
26 | const replyInfo = await ReplyService.getInstance().getReplyInfo(replyId);
27 | const { messageId } = replyInfo.message;
28 | const { chatroomId } = replyInfo.message.chatroom;
29 | await ReplyService.getInstance().deleteReply(replyId);
30 | io.to(String(chatroomId)).emit(eventName.DELETE_REPLY, { replyId, messageId, chatroomId });
31 | }
32 | };
33 |
34 | export default replyHandler;
35 |
--------------------------------------------------------------------------------
/server/src/socket/handler/reply-reaction-handler.ts:
--------------------------------------------------------------------------------
1 | import ReplyReactionService from '@service/reply-reaction-service';
2 | import MessageService from '@service/message-service';
3 | import ReplyService from '@service/reply-service';
4 | import eventName from '@constants/event-name';
5 |
6 | const replyHandler = {
7 | async createReplyReaction(io, socket, messageReaction) {
8 | const req = socket.request;
9 | const { userId } = req.user;
10 | const { replyId, title, emoji } = messageReaction;
11 | const newReplyReaction = await ReplyReactionService.getInstance().createReplyReaction(Number(userId), Number(replyId), title, emoji);
12 | const { messageId } = (await ReplyService.getInstance().getReplyInfo(Number(replyId))).message;
13 | const { chatroomId } = (await MessageService.getInstance().getMessage(messageId)).chatroom;
14 | io.to(String(chatroomId)).emit(eventName.CREATE_REPLY_REACTION, { ...newReplyReaction, messageId });
15 | },
16 |
17 | async deleteReplytReaction(io, socket, replyReaction) {
18 | const req = socket.request;
19 | const { userId } = req.user;
20 | const { replyId, reactionId } = replyReaction;
21 | await ReplyReactionService.getInstance().deleteReplyReaction(Number(userId), Number(replyId), Number(reactionId));
22 | const newReplyReaction = await ReplyReactionService.getInstance().getReplyReaction(Number(replyId), Number(reactionId));
23 | const { messageId } = (await ReplyService.getInstance().getReplyInfo(Number(replyId))).message;
24 | const { chatroomId } = (await MessageService.getInstance().getMessage(messageId)).chatroom;
25 | io.to(String(chatroomId)).emit(eventName.DELETE_REPLY_REACTION, { ...newReplyReaction, messageId });
26 | }
27 | };
28 |
29 | export default replyHandler;
30 |
--------------------------------------------------------------------------------
/server/src/socket/handler/socket-handler.ts:
--------------------------------------------------------------------------------
1 | import SocketService from '@service/socket-service';
2 |
3 | const socketHandler = {
4 | async saveSocketId(io, socket) {
5 | const req = socket.request;
6 | const { userId } = req.user;
7 | const socketId = socket.id;
8 | SocketService.getInstance().createSocket(userId, socketId);
9 | },
10 |
11 | async deleteSocketId(io, socket) {
12 | const socketId = socket.id;
13 | SocketService.getInstance().deleteSocket(socketId);
14 | }
15 | };
16 |
17 | export default socketHandler;
18 |
--------------------------------------------------------------------------------
/server/src/socket/index.ts:
--------------------------------------------------------------------------------
1 | import connectionEvent from '@socket/event/connection-event';
2 | import messageEvent from '@socket/event/message-event';
3 | import replyEvent from '@socket/event/reply-event';
4 | import chatroomEvent from '@socket/event/chatroom-event';
5 | import messageReactionEvent from '@socket/event/message-reaction-event';
6 | import disconnectEvent from '@socket/event/disconnect-event';
7 | import replyReactionEvent from '@socket/event/reply-rection-event';
8 |
9 | function socketIndex(io, socket) {
10 | connectionEvent(io, socket);
11 | disconnectEvent(io, socket);
12 | messageEvent(io, socket);
13 | replyEvent(io, socket);
14 | chatroomEvent(io, socket);
15 | messageReactionEvent(io, socket);
16 | replyReactionEvent(io, socket);
17 | }
18 |
19 | export default socketIndex;
20 |
--------------------------------------------------------------------------------
/server/src/socket/init-socket.ts:
--------------------------------------------------------------------------------
1 | import jwtMiddleware from '@socket/middleware/jwt';
2 | import socketIndex from '@socket/index';
3 |
4 | const initSocketIo = (io) => {
5 | jwtMiddleware(io);
6 | io.on('connection', (socket) => socketIndex(io, socket));
7 | };
8 |
9 | export default initSocketIo;
10 |
--------------------------------------------------------------------------------
/server/src/socket/middleware/jwt.ts:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 |
3 | const jwtMiddleware = (io) => {
4 | const wrap = (middleware) => (socket, next) => {
5 | const { request } = socket;
6 | const { token } = socket.handshake.query;
7 | request.headers.authorization = token;
8 | middleware(request, {}, next);
9 | };
10 | io.use(wrap(passport.authenticate('jwt', { session: false })));
11 | };
12 |
13 | export default jwtMiddleware;
14 |
--------------------------------------------------------------------------------
/server/src/socket/socket.ts:
--------------------------------------------------------------------------------
1 | import { Server } from 'socket.io';
2 |
3 | const Socket = (server) => {
4 | const io = new Server(server, {
5 | cors: {
6 | origin: '*',
7 | methods: ['GET', 'POST']
8 | }
9 | });
10 | return io;
11 | };
12 |
13 | export default Socket;
14 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2015", "es2016", "es2017", "es2018", "es2019", "es2020"],
4 | "outDir": "./dist",
5 | "target": "es5",
6 | "sourceMap": true,
7 |
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 |
14 | "baseUrl": ".",
15 | "paths": {
16 | "@constants/*": ["src/common/constants/*"],
17 | "@error/*": ["src/common/error/*"],
18 | "@middleware/*": ["src/common/middleware/*"],
19 | "@utils/*": ["src/common/utils/*"],
20 | "@controller/*": ["src/controller/*"],
21 | "@model/*": ["src/model/*"],
22 | "@repository/*": ["src/repository/*"],
23 | "@router/*": ["src/router/*"],
24 | "@service/*": ["src/service/*"],
25 | "@config/*":["src/common/config/*"],
26 | "@socket/*":["src/socket/*"]
27 | }
28 | },
29 | }
--------------------------------------------------------------------------------