From 8abf6177b580b24174b3056acc4ca5f3d5d7e326 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 30 Oct 2023 15:16:19 +0600 Subject: [PATCH] add comments for guests --- src/components/comment/Comment/index.tsx | 2 +- .../comment/CommentAvatar/index.tsx | 6 +- .../comment/CommentContent/index.tsx | 5 +- .../comment/CommentEditingForm/index.tsx | 38 ++++ src/components/comment/CommentForm/index.tsx | 162 +++++++++--------- .../containers/CommentWrapper/index.tsx | 7 +- .../CommentWrapper/styles.module.scss | 6 +- src/components/node/NodeCommentForm/index.tsx | 22 --- src/components/node/NodeCommentForm/ssr.ts | 8 - src/containers/boris/BorisComments/index.tsx | 24 +-- src/containers/node/NodeBottomBlock/index.tsx | 17 +- src/containers/node/NodeCommentForm/index.tsx | 47 +++++ src/containers/node/NodeCommentForm/ssr.ts | 8 + src/hooks/comments/useCommentFormFormik.ts | 59 ++++--- src/hooks/comments/useNodeComments.ts | 45 +++-- src/utils/context/CommentContextProvider.tsx | 16 +- 16 files changed, 278 insertions(+), 194 deletions(-) create mode 100644 src/components/comment/CommentEditingForm/index.tsx delete mode 100644 src/components/node/NodeCommentForm/index.tsx delete mode 100644 src/components/node/NodeCommentForm/ssr.ts create mode 100644 src/containers/node/NodeCommentForm/index.tsx create mode 100644 src/containers/node/NodeCommentForm/ssr.ts diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index b95a9d7e..021a608f 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -21,7 +21,7 @@ type IProps = HTMLAttributes & { isSame?: boolean; canEdit?: boolean; highlighted?: boolean; - saveComment: (data: IComment) => Promise; + saveComment: (data: IComment) => Promise; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; }; diff --git a/src/components/comment/CommentAvatar/index.tsx b/src/components/comment/CommentAvatar/index.tsx index 26aaa9d3..909d59ca 100644 --- a/src/components/comment/CommentAvatar/index.tsx +++ b/src/components/comment/CommentAvatar/index.tsx @@ -7,11 +7,15 @@ import { IUser } from '~/types/auth'; import { path } from '~/utils/ramda'; interface Props { - user: IUser; + user?: IUser; className?: string; } const CommentAvatar: FC = ({ user, className }) => { + if (!user) { + return ; + } + return ( Promise; + saveComment: (data: IComment) => Promise; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; } @@ -98,7 +99,7 @@ const CommentContent: FC = memo( if (isEditing) { return ( - Promise; + onCancelEdit?: () => void; +} + +const CommentEditingForm: FC = ({ + saveComment, + comment, + onCancelEdit, +}) => { + const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments); + + return ( + + + + + + ); +}; + +export { CommentEditingForm }; diff --git a/src/components/comment/CommentForm/index.tsx b/src/components/comment/CommentForm/index.tsx index 23f53041..1ff23cb8 100644 --- a/src/components/comment/CommentForm/index.tsx +++ b/src/components/comment/CommentForm/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useState } from 'react'; +import { FC, useCallback, useMemo, useState } from 'react'; import { FormikProvider } from 'formik'; import { observer } from 'mobx-react-lite'; @@ -12,108 +12,116 @@ import { Button } from '~/components/input/Button'; import { UploadDropzone } from '~/components/upload/UploadDropzone'; import { ERROR_LITERAL } from '~/constants/errors'; import { EMPTY_COMMENT } from '~/constants/node'; -import { UploadSubject, UploadTarget } from '~/constants/uploads'; import { useCommentFormFormik } from '~/hooks/comments/useCommentFormFormik'; -import { useUploader } from '~/hooks/data/useUploader'; import { useInputPasteUpload } from '~/hooks/dom/useInputPasteUpload'; import { IComment, INode } from '~/types'; -import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider'; +import { + UploaderContextProvider, + useUploaderContext, +} from '~/utils/context/UploaderContextProvider'; import styles from './styles.module.scss'; interface IProps { comment?: IComment; - nodeId: INode['id']; - saveComment: (data: IComment) => Promise; + allowUploads?: boolean; + + saveComment: (data: IComment) => Promise; onCancelEdit?: () => void; } -const CommentForm: FC = observer(({ comment, nodeId, saveComment, onCancelEdit }) => { - const [textarea, setTextArea] = useState(null); - const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments, comment?.files); - const formik = useCommentFormFormik( - comment || EMPTY_COMMENT, - nodeId, - uploader, - saveComment, - onCancelEdit - ); - const isLoading = formik.isSubmitting || uploader.isUploading; - const isEditing = !!comment?.id; +const CommentForm: FC = observer( + ({ comment, allowUploads, saveComment, onCancelEdit }) => { + const [textarea, setTextArea] = useState(null); + const uploader = useUploaderContext(); - const clearError = useCallback(() => { - if (formik.status) { - formik.setStatus(''); - } + const formik = useCommentFormFormik( + comment || EMPTY_COMMENT, + uploader.files, + uploader.setFiles, + saveComment, + onCancelEdit, + ); + const isLoading = formik.isSubmitting || uploader.isUploading; + const isEditing = !!comment?.id; - if (formik.errors.text) { - formik.setErrors({ - ...formik.errors, - text: '', - }); - } - }, [formik]); + const clearError = useCallback(() => { + if (formik.status) { + formik.setStatus(''); + } - const error = formik.status || formik.errors.text; - const onPaste = useInputPasteUpload(uploader.uploadFiles); + if (formik.errors.text) { + formik.setErrors({ + ...formik.errors, + text: '', + }); + } + }, [formik]); - return ( - + const error = formik.status || formik.errors.text; + const onPaste = useInputPasteUpload(uploader.uploadFiles); + + return (
- -
- +
+ - {!!error && ( -
- {ERROR_LITERAL[error] || error} -
- )} -
+ {!!error && ( +
+ {ERROR_LITERAL[error] || error} +
+ )} +
- + {allowUploads && } -
+
+ {allowUploads && (
+ )} -
- {!!textarea && ( - - )} -
- - - -
- {isEditing && ( - - )} - - -
+
+ {!!textarea && ( + + )}
- + + + +
+ {isEditing && ( + + )} + + +
+
- - ); -}); + ); + }, +); export { CommentForm }; diff --git a/src/components/containers/CommentWrapper/index.tsx b/src/components/containers/CommentWrapper/index.tsx index b9aebf05..0cfa4dbd 100644 --- a/src/components/containers/CommentWrapper/index.tsx +++ b/src/components/containers/CommentWrapper/index.tsx @@ -10,7 +10,7 @@ import { DivProps } from '~/utils/types'; import styles from './styles.module.scss'; type IProps = DivProps & { - user: IUser; + user?: IUser; isEmpty?: boolean; isLoading?: boolean; isForm?: boolean; @@ -36,7 +36,10 @@ const CommentWrapper: FC = ({ {...props} >
- +
~{path(['username'], user)}
diff --git a/src/components/containers/CommentWrapper/styles.module.scss b/src/components/containers/CommentWrapper/styles.module.scss index d2ad88e8..9fd7db27 100644 --- a/src/components/containers/CommentWrapper/styles.module.scss +++ b/src/components/containers/CommentWrapper/styles.module.scss @@ -95,7 +95,7 @@ div.thumb_image { background-size: cover; flex: 0 0 $comment_height; will-change: transform; - cursor: pointer; + cursor: default; @include tablet { height: 32px; @@ -105,6 +105,10 @@ div.thumb_image { } } +.pointer { + cursor: pointer; +} + .thumb_user { display: none; flex: 1; diff --git a/src/components/node/NodeCommentForm/index.tsx b/src/components/node/NodeCommentForm/index.tsx deleted file mode 100644 index bf4e219d..00000000 --- a/src/components/node/NodeCommentForm/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { FC } from 'react'; - -import { CommentForm } from '~/components/comment/CommentForm'; -import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { IComment } from '~/types'; -import { IUser } from '~/types/auth'; - -export interface NodeCommentFormProps { - user: IUser; - nodeId?: number; - saveComment: (comment: IComment) => Promise; -} - -const NodeCommentForm: FC = ({ user, nodeId, saveComment }) => { - return ( - - - - ); -}; - -export { NodeCommentForm }; diff --git a/src/components/node/NodeCommentForm/ssr.ts b/src/components/node/NodeCommentForm/ssr.ts deleted file mode 100644 index fb7ae750..00000000 --- a/src/components/node/NodeCommentForm/ssr.ts +++ /dev/null @@ -1,8 +0,0 @@ -import dynamic from 'next/dynamic'; - -import type { NodeCommentFormProps } from './index'; - -export const NodeCommentFormSSR = dynamic( - () => import('./index').then(it => it.NodeCommentForm), - { ssr: false } -); diff --git a/src/containers/boris/BorisComments/index.tsx b/src/containers/boris/BorisComments/index.tsx index 7f17df1f..ce1e9d38 100644 --- a/src/containers/boris/BorisComments/index.tsx +++ b/src/containers/boris/BorisComments/index.tsx @@ -1,34 +1,20 @@ -import React, { FC } from 'react'; +import { FC } from 'react'; import { Group } from '~/components/containers/Group'; import { Footer } from '~/components/main/Footer'; -import { NodeCommentFormSSR } from '~/components/node/NodeCommentForm/ssr'; import { NodeNoComments } from '~/components/node/NodeNoComments'; -import { isSSR } from '~/constants/ssr'; +import { NodeCommentFormSSR } from '~/containers/node/NodeCommentForm/ssr'; import { NodeComments } from '~/containers/node/NodeComments'; -import { useAuth } from '~/hooks/auth/useAuth'; import { useCommentContext } from '~/utils/context/CommentContextProvider'; -import { useNodeContext } from '~/utils/context/NodeContextProvider'; -import { useUserContext } from '~/utils/context/UserContextProvider'; -interface IProps {} - -const BorisComments: FC = () => { - const user = useUserContext(); - const { isUser } = useAuth(); +interface Props {} +const BorisComments: FC = () => { const { isLoading, comments, onSaveComment } = useCommentContext(); - const { node } = useNodeContext(); return ( - {(isUser || isSSR) && ( - - )} + {isLoading || !comments?.length ? ( diff --git a/src/containers/node/NodeBottomBlock/index.tsx b/src/containers/node/NodeBottomBlock/index.tsx index 22c8ba50..6ce7883a 100644 --- a/src/containers/node/NodeBottomBlock/index.tsx +++ b/src/containers/node/NodeBottomBlock/index.tsx @@ -1,24 +1,21 @@ -import React, { FC } from 'react'; +import { FC } from 'react'; -import { Card } from '~/components/containers/Card'; import { Filler } from '~/components/containers/Filler'; import { Group } from '~/components/containers/Group'; import { Padder } from '~/components/containers/Padder'; import { Sticky } from '~/components/containers/Sticky'; import { NodeAuthorBlock } from '~/components/node/NodeAuthorBlock'; -import { NodeCommentFormSSR } from '~/components/node/NodeCommentForm/ssr'; import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock'; import { NodeTagsBlock } from '~/components/node/NodeTagsBlock'; import { NodeBacklinks } from '~/containers/node/NodeBacklinks'; +import { NodeCommentFormSSR } from '~/containers/node/NodeCommentForm/ssr'; import { NodeComments } from '~/containers/node/NodeComments'; import { useNodeBlocks } from '~/hooks/node/useNodeBlocks'; import { useCommentContext } from '~/utils/context/CommentContextProvider'; import { useNodeContext } from '~/utils/context/NodeContextProvider'; import { useNodeRelatedContext } from '~/utils/context/NodeRelatedContextProvider'; -import { useUserContext } from '~/utils/context/UserContextProvider'; -import { useAuthProvider } from '~/utils/providers/AuthProvider'; import styles from './styles.module.scss'; @@ -27,7 +24,6 @@ interface IProps { } const NodeBottomBlock: FC = ({ commentsOrder }) => { - const user = useUserContext(); const { node, isLoading, backlinks } = useNodeContext(); const { comments, @@ -36,7 +32,6 @@ const NodeBottomBlock: FC = ({ commentsOrder }) => { } = useCommentContext(); const { related, isLoading: isLoadingRelated } = useNodeRelatedContext(); const { inline } = useNodeBlocks(node, isLoading); - const { isUser } = useAuthProvider(); if (node.deleted_at) { return ; @@ -59,13 +54,7 @@ const NodeBottomBlock: FC = ({ commentsOrder }) => { )} - {isUser && !isLoading && ( - - )} +
diff --git a/src/containers/node/NodeCommentForm/index.tsx b/src/containers/node/NodeCommentForm/index.tsx new file mode 100644 index 00000000..dcb71055 --- /dev/null +++ b/src/containers/node/NodeCommentForm/index.tsx @@ -0,0 +1,47 @@ +import { FC, useCallback } from 'react'; + +import { CommentForm } from '~/components/comment/CommentForm'; +import { CommentWrapper } from '~/components/containers/CommentWrapper'; +import { UploadDropzone } from '~/components/upload/UploadDropzone'; +import { EMPTY_USER } from '~/constants/auth'; +import { Dialog } from '~/constants/modal'; +import { UploadSubject, UploadTarget } from '~/constants/uploads'; +import { useAuth } from '~/hooks/auth/useAuth'; +import { useUploader } from '~/hooks/data/useUploader'; +import { useShowModal } from '~/hooks/modal/useShowModal'; +import { IComment } from '~/types'; +import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider'; + +export interface Props { + saveComment: (comment: IComment) => Promise; +} + +const NodeCommentForm: FC = ({ saveComment }) => { + const { user, isUser } = useAuth(); + const showLoginDialog = useShowModal(Dialog.Login); + + const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments); + const onCommentSave = useCallback( + async (comment: IComment) => { + if (!isUser) { + showLoginDialog({}); + return; + } + + return saveComment(comment); + }, + [isUser, showLoginDialog, saveComment], + ); + + return ( + + + + + + + + ); +}; + +export { NodeCommentForm }; diff --git a/src/containers/node/NodeCommentForm/ssr.ts b/src/containers/node/NodeCommentForm/ssr.ts new file mode 100644 index 00000000..9b2e02fa --- /dev/null +++ b/src/containers/node/NodeCommentForm/ssr.ts @@ -0,0 +1,8 @@ +import dynamic from 'next/dynamic'; + +import type { Props } from './index'; + +export const NodeCommentFormSSR = dynamic( + () => import('./index').then((it) => it.NodeCommentForm), + { ssr: false }, +); diff --git a/src/hooks/comments/useCommentFormFormik.ts b/src/hooks/comments/useCommentFormFormik.ts index d592e0aa..479bbe50 100644 --- a/src/hooks/comments/useCommentFormFormik.ts +++ b/src/hooks/comments/useCommentFormFormik.ts @@ -3,59 +3,62 @@ import { useCallback, useEffect, useRef } from 'react'; import { FormikHelpers, useFormik, useFormikContext } from 'formik'; import { array, object, string } from 'yup'; -import { IComment, INode } from '~/types'; -import { Uploader } from '~/utils/context/UploaderContextProvider'; +import { IComment, IFile } from '~/types'; +import { getErrorMessage } from '~/utils/errors/getErrorMessage'; import { showErrorToast } from '~/utils/errors/showToast'; -import { hasPath, path } from '~/utils/ramda'; const validationSchema = object().shape({ text: string(), files: array(), }); -const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers) => ( - error?: unknown -) => { - setSubmitting(false); +const onSuccess = + ({ resetForm, setSubmitting, setErrors }: FormikHelpers) => + (error?: unknown) => { + setSubmitting(false); + const message = getErrorMessage(error); - if (hasPath(['response', 'data', 'error'], error)) { - const message = path(['response', 'data', 'error'], error) as string; - setErrors({ text: message }); - showErrorToast(error); - return; - } + if (message) { + setErrors({ text: message }); + showErrorToast(error); + return; + } - if (resetForm) { - resetForm(); - } -}; + if (resetForm) { + resetForm(); + } + }; export const useCommentFormFormik = ( - values: IComment, - nodeId: INode['id'], - uploader: Uploader, - sendData: (data: IComment) => Promise, - stopEditing?: () => void + comment: IComment, + files: IFile[], + setFiles: (file: IFile[]) => void, + sendData: (data: IComment) => Promise, + stopEditing?: () => void, ) => { - const { current: initialValues } = useRef(values); + const { current: initialValues } = useRef(comment); const onSubmit = useCallback( async (values: IComment, helpers: FormikHelpers) => { try { helpers.setSubmitting(true); - await sendData({ ...values, files: uploader.files }); - onSuccess(helpers)(); + + const comment = await sendData({ ...values, files }); + + if (comment) { + onSuccess(helpers)(); + } } catch (error) { onSuccess(helpers)(error); } }, - [sendData, uploader.files] + [sendData, files], ); const onReset = useCallback(() => { - uploader.setFiles([]); + setFiles([]); if (stopEditing) stopEditing(); - }, [stopEditing, uploader]); + }, [stopEditing, setFiles]); const formik = useFormik({ initialValues, diff --git a/src/hooks/comments/useNodeComments.ts b/src/hooks/comments/useNodeComments.ts index c7e2063d..428e2871 100644 --- a/src/hooks/comments/useNodeComments.ts +++ b/src/hooks/comments/useNodeComments.ts @@ -26,17 +26,19 @@ export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => { } await mutate( - prev => - prev?.map(list => - list.map(comment => (comment.id === id ? { ...comment, deleted_at } : comment)) + (prev) => + prev?.map((list) => + list.map((comment) => + comment.id === id ? { ...comment, deleted_at } : comment, + ), ), - false + false, ); } catch (error) { showErrorToast(error); } }, - [data, mutate, nodeId] + [data, mutate, nodeId], ); const onEdit = useCallback( @@ -50,22 +52,37 @@ export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => { // Comment was created if (!comment.id) { await mutate( - data.map((list, index) => (index === 0 ? [result.comment, ...list] : list)), - false + data.map((list, index) => + index === 0 ? [result.comment, ...list] : list, + ), + false, ); - return; + + return result.comment; } await mutate( - prev => - prev?.map(list => - list.map(it => (it.id === result.comment.id ? { ...it, ...result.comment } : it)) + (prev) => + prev?.map((list) => + list.map((it) => + it.id === result.comment.id ? { ...it, ...result.comment } : it, + ), ), - false + false, ); + + return result.comment; }, - [data, mutate, nodeId] + [data, mutate, nodeId], ); - return { onLoadMoreComments, onDelete, comments, hasMore, isLoading, onEdit, isLoadingMore }; + return { + onLoadMoreComments, + onDelete, + comments, + hasMore, + isLoading, + onEdit, + isLoadingMore, + }; }; diff --git a/src/utils/context/CommentContextProvider.tsx b/src/utils/context/CommentContextProvider.tsx index 3181ffc6..fda2631f 100644 --- a/src/utils/context/CommentContextProvider.tsx +++ b/src/utils/context/CommentContextProvider.tsx @@ -10,25 +10,31 @@ export interface CommentProviderProps { isLoadingMore: boolean; onShowImageModal: (images: IFile[], index: number) => void; onLoadMoreComments: () => void; - onSaveComment: (comment: IComment) => Promise; + onSaveComment: (comment: IComment) => Promise; onDeleteComment: (id: IComment['id'], isLocked: boolean) => void; } const CommentContext = createContext({ - // user: EMPTY_USER, comments: [], hasMore: false, lastSeenCurrent: null, isLoading: false, isLoadingMore: false, - onSaveComment: async () => {}, + onSaveComment: async () => undefined, onShowImageModal: () => {}, onLoadMoreComments: () => {}, onDeleteComment: () => {}, }); -export const CommentContextProvider: FC = ({ children, ...contextValue }) => { - return {children}; +export const CommentContextProvider: FC = ({ + children, + ...contextValue +}) => { + return ( + + {children} + + ); }; export const useCommentContext = () => useContext(CommentContext);