From fc89fca8bbf41a9e984dda0f1b5139d782195c10 Mon Sep 17 00:00:00 2001 From: MangriMen <49300253+MangriMen@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:39:14 +0700 Subject: [PATCH] feat: DF-102 | imrpove comment input (#104) * feat: DF-102 | add multiline to post and comments * feat: DF-102 | add sending comment on enter * feat: DF-102 | fix comment send button on the bottom * feat: DF-102 | add emoji picker to post and comment creation * feat: DF-102 | fix lockfile conflict --- package.json | 3 + pnpm-lock.yaml | 35 ++++++- src/components/post/Comment/styles.tsx | 1 + .../post/CreationDialog/CreatePostForm.tsx | 31 ++++++- src/components/post/EmojiButton.tsx | 59 ++++++++++++ .../post/PostCard/PostCardInput.tsx | 91 ++++++++++++++----- src/components/post/PostCard/schemas.ts | 5 + src/components/post/PostCard/styles.tsx | 1 + src/consts/post.ts | 2 + src/locales/ru/translation.json | 1 + 10 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 src/components/post/EmojiButton.tsx create mode 100644 src/components/post/PostCard/schemas.ts diff --git a/package.json b/package.json index cbb69a4..f206019 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@emoji-mart/react": "^1.1.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@hookform/resolvers": "^2.9.10", @@ -18,6 +19,7 @@ "@reduxjs/toolkit": "^1.9.2", "axios": "^1.3.4", "axios-case-converter": "^1.0.1", + "emoji-mart": "^5.5.2", "i18next": "^22.4.10", "mui-modal-provider": "^2.2.0", "notistack": "^3.0.1", @@ -33,6 +35,7 @@ }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.0", + "@types/emoji-mart": "^3.0.9", "@types/node": "^18.14.0", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f64bb14..a59e091 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,13 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false dependencies: + '@emoji-mart/react': + specifier: ^1.1.1 + version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) '@emotion/react': specifier: ^11.10.6 version: 11.10.6(@types/react@18.0.27)(react@18.2.0) @@ -28,6 +35,9 @@ dependencies: axios-case-converter: specifier: ^1.0.1 version: 1.0.1(axios@1.3.4) + emoji-mart: + specifier: ^5.5.2 + version: 5.5.2 i18next: specifier: ^22.4.10 version: 22.4.10 @@ -69,6 +79,9 @@ devDependencies: '@trivago/prettier-plugin-sort-imports': specifier: ^4.1.0 version: 4.1.0(prettier@2.8.3) + '@types/emoji-mart': + specifier: ^3.0.9 + version: 3.0.9 '@types/node': specifier: ^18.14.0 version: 18.14.0 @@ -257,6 +270,16 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + /@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0): + resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} + peerDependencies: + emoji-mart: ^5.2 + react: ^16.8 || ^17 || ^18 + dependencies: + emoji-mart: 5.5.2 + react: 18.2.0 + dev: false + /@emotion/babel-plugin@11.10.8: resolution: {integrity: sha512-gxNky50AJL3AlkbjvTARiwAqei6/tNUxDZPSKd+3jqWVM3AmdVTTdpjHorR/an/M0VJqdsuq5oGcFH+rjtyujQ==} dependencies: @@ -1001,6 +1024,12 @@ packages: - supports-color dev: true + /@types/emoji-mart@3.0.9: + resolution: {integrity: sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==} + dependencies: + '@types/react': 18.0.27 + dev: true + /@types/hoist-non-react-statics@3.3.1: resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} dependencies: @@ -1660,6 +1689,10 @@ packages: tslib: 2.5.0 dev: false + /emoji-mart@5.5.2: + resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + dev: false + /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true diff --git a/src/components/post/Comment/styles.tsx b/src/components/post/Comment/styles.tsx index 70f4ad5..b98b8c6 100644 --- a/src/components/post/Comment/styles.tsx +++ b/src/components/post/Comment/styles.tsx @@ -38,6 +38,7 @@ export const CommentBody = styled(Span)` export const CommentText = styled(Typography)` float: left; + white-space: pre-wrap; overflow-wrap: break-word; font-size: ${props => props.theme.typography.body2.fontSize}; padding: 0 0.25rem; diff --git a/src/components/post/CreationDialog/CreatePostForm.tsx b/src/components/post/CreationDialog/CreatePostForm.tsx index 9ee05ae..9c4e12f 100644 --- a/src/components/post/CreationDialog/CreatePostForm.tsx +++ b/src/components/post/CreationDialog/CreatePostForm.tsx @@ -1,5 +1,5 @@ import '@mui/material'; -import { Box, TextField, styled } from '@mui/material'; +import { Box, InputAdornment, TextField, styled } from '@mui/material'; import { StyledButton } from 'components/common'; import { ImageUpload } from 'components/common/FileUpload/ImageUpload'; import { PostCardContent, PostCardStyled } from 'components/post/PostCard'; @@ -10,6 +10,7 @@ import { } from 'consts'; import { useDataMutation } from 'ducks/data/api'; import { useCreatePostMutation } from 'ducks/post/api'; +import { BaseEmoji } from 'emoji-mart/dist-es'; import { OptionsObject, useSnackbar } from 'notistack'; import { ChangeEvent, useEffect, useState } from 'react'; import { @@ -20,6 +21,7 @@ import { } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { EmojiButton } from '../EmojiButton'; import { CreatePostFormProps, PostForm } from './interfaces'; const defaultValues = { @@ -93,6 +95,13 @@ export const CreatePostForm = ({ onClose }: CreatePostFormProps) => { } }, [disable]); + const handleEmojiSelect = (emoji: BaseEmoji) => { + form.setValue( + 'description', + `${form.getValues('description')}${emoji.native}`, + ); + }; + const onSubmitHandler: SubmitHandler = async data => { setDisable(true); @@ -146,7 +155,25 @@ export const CreatePostForm = ({ onClose }: CreatePostFormProps) => { label={t('postDescription')} multiline maxRows={POST_DESCRIPTION_MAX_ROWS} - InputProps={{ disableUnderline: true }} + InputProps={{ + disableUnderline: true, + endAdornment: ( + + + + ), + }} inputProps={{ maxLength: SHAPE_CONSTRAINTS.DESCRIPTION_MAX, }} diff --git a/src/components/post/EmojiButton.tsx b/src/components/post/EmojiButton.tsx new file mode 100644 index 0000000..ca2904a --- /dev/null +++ b/src/components/post/EmojiButton.tsx @@ -0,0 +1,59 @@ +import Picker from '@emoji-mart/react'; +import SentimentSatisfiedOutlinedIcon from '@mui/icons-material/SentimentSatisfiedOutlined'; +import { + IconButton, + IconButtonProps, + Popover, + PopoverProps, +} from '@mui/material'; +import { BaseEmoji } from 'emoji-mart/dist-es'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const EmojiButton = ({ + onEmojiSelect, + PopoverProps, + ...props +}: { + onEmojiSelect: (emoji: BaseEmoji) => void; + PopoverProps?: Omit; +} & IconButtonProps) => { + const { t, i18n } = useTranslation('translation', { keyPrefix: 'comment' }); + + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + return ( + <> + + + + + + + + ); +}; diff --git a/src/components/post/PostCard/PostCardInput.tsx b/src/components/post/PostCard/PostCardInput.tsx index f256c0b..747775c 100644 --- a/src/components/post/PostCard/PostCardInput.tsx +++ b/src/components/post/PostCard/PostCardInput.tsx @@ -1,10 +1,17 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import SendIcon from '@mui/icons-material/Send'; import { IconButton, InputBase, Paper, styled } from '@mui/material'; import { PostProps } from 'components/post'; +import { POST_INPUT_MAX_ROWS } from 'consts'; import { useSendCommentMutation } from 'ducks/comment/api'; -import { ChangeEvent, FormEvent, useCallback, useState } from 'react'; +import { BaseEmoji } from 'emoji-mart/dist-es'; +import { KeyboardEvent, useState } from 'react'; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { EmojiButton } from '../EmojiButton'; +import { commentValidator } from './schemas'; + const PaperStyled = styled(Paper)` display: flex; align-items: center; @@ -17,48 +24,82 @@ const InputStyled = styled(InputBase)` font-size: 16px; `; +interface InputValues { + content: string; +} + +const defaultValues = { + content: '', +}; + export const PostCardInput = ({ post }: PostProps) => { const { t } = useTranslation('translation', { keyPrefix: 'comment' }); - const [isSendDisabled, setIsSendDisabled] = useState(false); + const { control, handleSubmit, reset, getValues, setValue } = + useForm({ + defaultValues: defaultValues, + resolver: yupResolver(commentValidator), + }); - const [commentText, setCommentText] = useState(''); + const [isSendDisabled, setIsSendDisabled] = useState(false); const [sendComment] = useSendCommentMutation(); - const handleInputChange = useCallback((e: ChangeEvent) => { - setCommentText(e.target.value); - }, []); + const handleEmojiSelect = (emoji: BaseEmoji) => { + setValue('content', `${getValues('content')}${emoji.native}`); + }; - const handleSendComment = useCallback( - async (e: FormEvent) => { - e.preventDefault(); + const handleInputEnter = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSubmit(handleSendComment)(); + return false; + } + }; - setIsSendDisabled(true); + const handleSendComment: SubmitHandler = async data => { + setIsSendDisabled(true); - await sendComment({ - path: { post: post.id }, - body: { content: commentText }, - }); + await sendComment({ + path: { post: post.id }, + body: data, + }); - setCommentText(''); - setIsSendDisabled(false); - }, - [commentText, post.id, sendComment], - ); + setIsSendDisabled(false); + + reset(); + }; return ( - - + ( + + )} + /> + diff --git a/src/components/post/PostCard/schemas.ts b/src/components/post/PostCard/schemas.ts new file mode 100644 index 0000000..25ada0e --- /dev/null +++ b/src/components/post/PostCard/schemas.ts @@ -0,0 +1,5 @@ +import * as yup from 'yup'; + +export const commentValidator = yup.object().shape({ + content: yup.string().required(), +}); diff --git a/src/components/post/PostCard/styles.tsx b/src/components/post/PostCard/styles.tsx index 1a5181b..be90a05 100644 --- a/src/components/post/PostCard/styles.tsx +++ b/src/components/post/PostCard/styles.tsx @@ -119,6 +119,7 @@ export const PostCardDescriptionCollapse = styled(Collapse)< `; export const PostCardDescriptionText = styled(Typography)` + white-space: pre-wrap; overflow-wrap: break-word; font-size: 14px; padding: 0 4px; diff --git a/src/consts/post.ts b/src/consts/post.ts index ecfda8a..16a0300 100644 --- a/src/consts/post.ts +++ b/src/consts/post.ts @@ -26,6 +26,8 @@ export const COMMENTS_FETCH_COUNT = { export const POST_DESCRIPTION_MAX_ROWS = 18; export const POST_DESCRIPTION_COLLAPSED_SIZE = '6.5625rem'; +export const POST_INPUT_MAX_ROWS = 5; + export const POSTS_LOADING_OFFSET_HEIGHT = 320; export const POST_WITHOUT_SCROLL_FETCH_DELAY = 100; diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 389cf8f..f04175c 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -59,6 +59,7 @@ "close": "Закрыть" }, "comment": { + "emoji": "Эмодзи", "writeComment": "Написать комментарий...", "edit": "Редактировать", "delete": "Удалить",