1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

add comments for guests

This commit is contained in:
Fedor Katurov 2023-10-30 15:16:19 +06:00
parent cbf7b1f616
commit 8abf6177b5
16 changed files with 278 additions and 194 deletions

View file

@ -21,7 +21,7 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
isSame?: boolean;
canEdit?: boolean;
highlighted?: boolean;
saveComment: (data: IComment) => Promise<unknown>;
saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (id: IComment['id'], isLocked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void;
};

View file

@ -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<Props> = ({ user, className }) => {
if (!user) {
return <Avatar className={className} />;
}
return (
<MenuButton
position="auto"

View file

@ -22,6 +22,7 @@ import { IComment, IFile } from '~/types';
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
import { append, assocPath, path, reduce } from '~/utils/ramda';
import { CommentEditingForm } from '../CommentEditingForm';
import { CommentImageGrid } from '../CommentImageGrid';
import { CommentMenu } from '../CommentMenu';
@ -32,7 +33,7 @@ interface IProps {
nodeId: number;
comment: IComment;
canEdit: boolean;
saveComment: (data: IComment) => Promise<unknown>;
saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (id: IComment['id'], isLocked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void;
}
@ -98,7 +99,7 @@ const CommentContent: FC<IProps> = memo(
if (isEditing) {
return (
<CommentForm
<CommentEditingForm
saveComment={saveComment}
nodeId={nodeId}
comment={comment}

View file

@ -0,0 +1,38 @@
import { FC } from 'react';
import { CommentForm } from '~/components/comment/CommentForm';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { useUploader } from '~/hooks/data/useUploader';
import { IComment, INode } from '~/types';
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
interface CommentEditingFormProps {
comment: IComment;
nodeId: INode['id'];
saveComment: (data: IComment) => Promise<IComment | undefined>;
onCancelEdit?: () => void;
}
const CommentEditingForm: FC<CommentEditingFormProps> = ({
saveComment,
comment,
onCancelEdit,
}) => {
const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments);
return (
<UploadDropzone onUpload={uploader.uploadFiles}>
<UploaderContextProvider value={uploader}>
<CommentForm
saveComment={saveComment}
comment={comment}
onCancelEdit={onCancelEdit}
allowUploads
/>
</UploaderContextProvider>
</UploadDropzone>
);
};
export { CommentEditingForm };

View file

@ -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,31 +12,35 @@ 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<unknown>;
allowUploads?: boolean;
saveComment: (data: IComment) => Promise<IComment | undefined>;
onCancelEdit?: () => void;
}
const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCancelEdit }) => {
const CommentForm: FC<IProps> = observer(
({ comment, allowUploads, saveComment, onCancelEdit }) => {
const [textarea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments, comment?.files);
const uploader = useUploaderContext();
const formik = useCommentFormFormik(
comment || EMPTY_COMMENT,
nodeId,
uploader,
uploader.files,
uploader.setFiles,
saveComment,
onCancelEdit
onCancelEdit,
);
const isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id;
@ -58,10 +62,8 @@ const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCanc
const onPaste = useInputPasteUpload(uploader.uploadFiles);
return (
<UploadDropzone onUpload={uploader.uploadFiles}>
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
<FormikProvider value={formik}>
<UploaderContextProvider value={uploader}>
<div className={styles.input}>
<LocalCommentFormTextarea onPaste={onPaste} ref={setTextArea} />
@ -72,12 +74,14 @@ const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCanc
)}
</div>
<CommentFormAttaches />
{allowUploads && <CommentFormAttaches />}
<div className={styles.buttons}>
{allowUploads && (
<div className={styles.button_column}>
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
</div>
)}
<div className={styles.button_column}>
{!!textarea && (
@ -92,7 +96,12 @@ const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCanc
<div className={styles.button_column}>
{isEditing && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
<Button
size="small"
color="link"
type="button"
onClick={onCancelEdit}
>
Отмена
</Button>
)}
@ -109,11 +118,10 @@ const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCanc
</Button>
</div>
</div>
</UploaderContextProvider>
</FormikProvider>
</form>
</UploadDropzone>
);
});
},
);
export { CommentForm };

View file

@ -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<IProps> = ({
{...props}
>
<div className={styles.thumb}>
<CommentAvatar user={user} className={styles.thumb_image} />
<CommentAvatar
user={user}
className={classNames(styles.thumb_image, { [styles.pointer]: user })}
/>
<div className={styles.thumb_user}>~{path(['username'], user)}</div>
</div>

View file

@ -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;

View file

@ -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<unknown>;
}
const NodeCommentForm: FC<NodeCommentFormProps> = ({ user, nodeId, saveComment }) => {
return (
<CommentWrapper user={user} isForm>
<CommentForm nodeId={nodeId} saveComment={saveComment} />
</CommentWrapper>
);
};
export { NodeCommentForm };

View file

@ -1,8 +0,0 @@
import dynamic from 'next/dynamic';
import type { NodeCommentFormProps } from './index';
export const NodeCommentFormSSR = dynamic<NodeCommentFormProps>(
() => import('./index').then(it => it.NodeCommentForm),
{ ssr: false }
);

View file

@ -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<IProps> = () => {
const user = useUserContext();
const { isUser } = useAuth();
interface Props {}
const BorisComments: FC<Props> = () => {
const { isLoading, comments, onSaveComment } = useCommentContext();
const { node } = useNodeContext();
return (
<Group>
{(isUser || isSSR) && (
<NodeCommentFormSSR
user={user}
nodeId={node.id}
saveComment={onSaveComment}
/>
)}
<NodeCommentFormSSR saveComment={onSaveComment} />
{isLoading || !comments?.length ? (
<NodeNoComments loading count={7} />

View file

@ -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<IProps> = ({ commentsOrder }) => {
const user = useUserContext();
const { node, isLoading, backlinks } = useNodeContext();
const {
comments,
@ -36,7 +32,6 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
} = useCommentContext();
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
const { inline } = useNodeBlocks(node, isLoading);
const { isUser } = useAuthProvider();
if (node.deleted_at) {
return <NodeDeletedBadge />;
@ -59,13 +54,7 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
)}
</article>
{isUser && !isLoading && (
<NodeCommentFormSSR
nodeId={node.id}
saveComment={onSaveComment}
user={user}
/>
)}
<NodeCommentFormSSR saveComment={onSaveComment} />
<div className={styles.subheader}>
<Filler className={styles.backlinks}>

View file

@ -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<IComment | undefined>;
}
const NodeCommentForm: FC<Props> = ({ 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 (
<UploadDropzone onUpload={uploader.uploadFiles}>
<UploaderContextProvider value={uploader}>
<CommentWrapper user={isUser ? user : undefined} isForm>
<CommentForm saveComment={onCommentSave} allowUploads={isUser} />
</CommentWrapper>
</UploaderContextProvider>
</UploadDropzone>
);
};
export { NodeCommentForm };

View file

@ -0,0 +1,8 @@
import dynamic from 'next/dynamic';
import type { Props } from './index';
export const NodeCommentFormSSR = dynamic<Props>(
() => import('./index').then((it) => it.NodeCommentForm),
{ ssr: false },
);

View file

@ -3,23 +3,22 @@ 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<IComment>) => (
error?: unknown
) => {
const onSuccess =
({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComment>) =>
(error?: unknown) => {
setSubmitting(false);
const message = getErrorMessage(error);
if (hasPath(['response', 'data', 'error'], error)) {
const message = path(['response', 'data', 'error'], error) as string;
if (message) {
setErrors({ text: message });
showErrorToast(error);
return;
@ -28,34 +27,38 @@ const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComme
if (resetForm) {
resetForm();
}
};
};
export const useCommentFormFormik = (
values: IComment,
nodeId: INode['id'],
uploader: Uploader,
sendData: (data: IComment) => Promise<unknown>,
stopEditing?: () => void
comment: IComment,
files: IFile[],
setFiles: (file: IFile[]) => void,
sendData: (data: IComment) => Promise<IComment | undefined>,
stopEditing?: () => void,
) => {
const { current: initialValues } = useRef(values);
const { current: initialValues } = useRef(comment);
const onSubmit = useCallback(
async (values: IComment, helpers: FormikHelpers<IComment>) => {
try {
helpers.setSubmitting(true);
await sendData({ ...values, files: uploader.files });
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,

View file

@ -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
);
},
[data, mutate, nodeId]
),
false,
);
return { onLoadMoreComments, onDelete, comments, hasMore, isLoading, onEdit, isLoadingMore };
return result.comment;
},
[data, mutate, nodeId],
);
return {
onLoadMoreComments,
onDelete,
comments,
hasMore,
isLoading,
onEdit,
isLoadingMore,
};
};

View file

@ -10,25 +10,31 @@ export interface CommentProviderProps {
isLoadingMore: boolean;
onShowImageModal: (images: IFile[], index: number) => void;
onLoadMoreComments: () => void;
onSaveComment: (comment: IComment) => Promise<unknown>;
onSaveComment: (comment: IComment) => Promise<IComment | undefined>;
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
}
const CommentContext = createContext<CommentProviderProps>({
// user: EMPTY_USER,
comments: [],
hasMore: false,
lastSeenCurrent: null,
isLoading: false,
isLoadingMore: false,
onSaveComment: async () => {},
onSaveComment: async () => undefined,
onShowImageModal: () => {},
onLoadMoreComments: () => {},
onDeleteComment: () => {},
});
export const CommentContextProvider: FC<CommentProviderProps> = ({ children, ...contextValue }) => {
return <CommentContext.Provider value={contextValue}>{children}</CommentContext.Provider>;
export const CommentContextProvider: FC<CommentProviderProps> = ({
children,
...contextValue
}) => {
return (
<CommentContext.Provider value={contextValue}>
{children}
</CommentContext.Provider>
);
};
export const useCommentContext = () => useContext(CommentContext);