mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
#34 fixed boris layout with hooks
This commit is contained in:
parent
29e5aef01b
commit
62f0fa59ca
19 changed files with 231 additions and 773 deletions
|
@ -1,10 +1,8 @@
|
|||
import React, { FC, HTMLAttributes, memo } from 'react';
|
||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||
import { ICommentGroup } from '~/redux/types';
|
||||
import { IComment, ICommentGroup } from '~/redux/types';
|
||||
import { CommentContent } from '~/components/comment/CommentContent';
|
||||
import styles from './styles.module.scss';
|
||||
import { nodeEditComment, nodeLockComment } from '~/redux/node/actions';
|
||||
import { INodeState } from '~/redux/node/reducer';
|
||||
import { CommendDeleted } from '../../node/CommendDeleted';
|
||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||
|
||||
|
@ -12,25 +10,21 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
|
|||
is_empty?: boolean;
|
||||
is_loading?: boolean;
|
||||
comment_group: ICommentGroup;
|
||||
comment_data: INodeState['comment_data'];
|
||||
is_same?: boolean;
|
||||
can_edit?: boolean;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onEdit: typeof nodeEditComment;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
||||
};
|
||||
|
||||
const Comment: FC<IProps> = memo(
|
||||
({
|
||||
comment_group,
|
||||
comment_data,
|
||||
is_empty,
|
||||
is_same,
|
||||
is_loading,
|
||||
className,
|
||||
can_edit,
|
||||
onDelete,
|
||||
onEdit,
|
||||
modalShowPhotoswipe,
|
||||
...props
|
||||
}) => {
|
||||
|
|
|
@ -10,17 +10,16 @@ import { AudioPlayer } from '~/components/media/AudioPlayer';
|
|||
import classnames from 'classnames';
|
||||
import { PRESETS } from '~/constants/urls';
|
||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||
import { nodeLockComment } from '~/redux/node/actions';
|
||||
import { CommentMenu } from '../CommentMenu';
|
||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
|
||||
import { CommentForm } from '~/components/comment/CommentForm';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
|
||||
interface IProps {
|
||||
comment: IComment;
|
||||
can_edit: boolean;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
||||
}
|
||||
|
||||
|
@ -59,7 +58,7 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalSho
|
|||
);
|
||||
|
||||
if (isEditing) {
|
||||
return <LocalCommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
|
||||
return <CommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,238 +1,97 @@
|
|||
import React, { FC, KeyboardEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import styles from './styles.module.scss';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
|
||||
import { FormikProvider } from 'formik';
|
||||
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import assocPath from 'ramda/es/assocPath';
|
||||
import { IComment, IFileWithUUID, InputHandler } from '~/redux/types';
|
||||
import { connect } from 'react-redux';
|
||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
import uuid from 'uuid4';
|
||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||
import { selectUploads } from '~/redux/uploads/selectors';
|
||||
import { IState } from '~/redux/store';
|
||||
import { getFileType } from '~/utils/uploader';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
|
||||
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
|
||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
||||
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { IComment, INode } from '~/redux/types';
|
||||
import { EMPTY_COMMENT } from '~/redux/node/constants';
|
||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||
import styles from './styles.module.scss';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
node: selectNode(state),
|
||||
uploads: selectUploads(state),
|
||||
});
|
||||
interface IProps {
|
||||
comment?: IComment;
|
||||
nodeId: INode['id'];
|
||||
onCancelEdit?: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
nodePostComment: NODE_ACTIONS.nodePostComment,
|
||||
nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit,
|
||||
nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData,
|
||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||
};
|
||||
const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||
const uploader = useFileUploader(
|
||||
UPLOAD_SUBJECTS.COMMENT,
|
||||
UPLOAD_TARGETS.COMMENTS,
|
||||
comment?.files
|
||||
);
|
||||
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
|
||||
const isLoading = formik.isSubmitting || uploader.isUploading;
|
||||
const isEditing = !!comment?.id;
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps & {
|
||||
id: number;
|
||||
is_before?: boolean;
|
||||
};
|
||||
const clearError = useCallback(() => {
|
||||
if (formik.status) {
|
||||
formik.setStatus('');
|
||||
}
|
||||
|
||||
const CommentFormUnconnected: FC<IProps> = memo(
|
||||
({
|
||||
node: { comment_data, is_sending_comment },
|
||||
uploads: { statuses, files },
|
||||
id,
|
||||
is_before = false,
|
||||
nodePostComment,
|
||||
nodeSetCommentData,
|
||||
uploadUploadFiles,
|
||||
nodeCancelCommentEdit,
|
||||
}) => {
|
||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||
const comment = useMemo(() => comment_data[id], [comment_data, id]);
|
||||
if (formik.errors.text) {
|
||||
formik.setErrors({
|
||||
...formik.errors,
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
}, [formik]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(files: File[]) => {
|
||||
const items: IFileWithUUID[] = files.map(
|
||||
(file: File): IFileWithUUID => ({
|
||||
file,
|
||||
temp_id: uuid(),
|
||||
subject: UPLOAD_SUBJECTS.COMMENT,
|
||||
target: UPLOAD_TARGETS.COMMENTS,
|
||||
type: getFileType(file),
|
||||
})
|
||||
);
|
||||
const error = formik.status || formik.errors.text;
|
||||
|
||||
const temps = items.map(file => file.temp_id);
|
||||
|
||||
nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment));
|
||||
uploadUploadFiles(items);
|
||||
},
|
||||
[uploadUploadFiles, comment, id, nodeSetCommentData]
|
||||
);
|
||||
|
||||
const onInput = useCallback<InputHandler>(
|
||||
text => {
|
||||
nodeSetCommentData(id, assocPath(['text'], text, comment));
|
||||
},
|
||||
[nodeSetCommentData, comment, id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const temp_ids = (comment && comment.temp_ids) || [];
|
||||
const added_files = temp_ids
|
||||
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
|
||||
.map(el => !!el && files[el])
|
||||
.filter(el => !!el && !comment.files.some(file => file && file.id === el.id));
|
||||
|
||||
const filtered_temps = temp_ids.filter(
|
||||
temp_id =>
|
||||
statuses[temp_id] &&
|
||||
(!statuses[temp_id].uuid || !added_files.some(file => file.id === statuses[temp_id].uuid))
|
||||
);
|
||||
|
||||
if (added_files.length) {
|
||||
nodeSetCommentData(id, {
|
||||
...comment,
|
||||
temp_ids: filtered_temps,
|
||||
files: [...comment.files, ...added_files],
|
||||
});
|
||||
}
|
||||
}, [statuses, files]);
|
||||
|
||||
const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
event => {
|
||||
if (event) event.preventDefault();
|
||||
if (isUploadingNow || is_sending_comment) return;
|
||||
|
||||
nodePostComment(id, is_before);
|
||||
},
|
||||
[nodePostComment, id, is_before, isUploadingNow, is_sending_comment]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||
({ ctrlKey, key }) => {
|
||||
if (!!ctrlKey && key === 'Enter') onSubmit(null);
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
||||
const images = useMemo(
|
||||
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
|
||||
[comment.files]
|
||||
);
|
||||
|
||||
const locked_images = useMemo(
|
||||
() =>
|
||||
comment.temp_ids
|
||||
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.IMAGE)
|
||||
.map(temp_id => statuses[temp_id]),
|
||||
[statuses, comment.temp_ids]
|
||||
);
|
||||
|
||||
const audios = useMemo(
|
||||
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
||||
[comment.files]
|
||||
);
|
||||
|
||||
const locked_audios = useMemo(
|
||||
() =>
|
||||
comment.temp_ids
|
||||
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.AUDIO)
|
||||
.map(temp_id => statuses[temp_id]),
|
||||
[statuses, comment.temp_ids]
|
||||
);
|
||||
|
||||
const onCancelEdit = useCallback(() => {
|
||||
nodeCancelCommentEdit(id);
|
||||
}, [nodeCancelCommentEdit, comment.id]);
|
||||
|
||||
const placeholder = useRandomPhrase('SIMPLE');
|
||||
|
||||
const clearError = useCallback(() => nodeSetCommentData(id, { error: '' }), [
|
||||
id,
|
||||
nodeSetCommentData,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (comment.error) clearError();
|
||||
}, [comment.files, comment.text]);
|
||||
|
||||
const setData = useCallback(
|
||||
(data: Partial<IComment>) => {
|
||||
nodeSetCommentData(id, data);
|
||||
},
|
||||
[nodeSetCommentData, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommentFormDropzone onUpload={onUpload}>
|
||||
<form onSubmit={onSubmit} className={styles.wrap}>
|
||||
return (
|
||||
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
||||
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
|
||||
<FormikProvider value={formik}>
|
||||
<FileUploaderProvider value={uploader}>
|
||||
<div className={styles.input}>
|
||||
<Textarea
|
||||
value={comment.text}
|
||||
handler={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={is_sending_comment}
|
||||
placeholder={placeholder}
|
||||
minRows={2}
|
||||
setRef={setTextarea}
|
||||
/>
|
||||
<LocalCommentFormTextarea setRef={setTextarea} />
|
||||
|
||||
{comment.error && (
|
||||
{!!error && (
|
||||
<div className={styles.error} onClick={clearError}>
|
||||
{ERROR_LITERAL[comment.error] || comment.error}
|
||||
{ERROR_LITERAL[error] || error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CommentFormAttaches
|
||||
images={images}
|
||||
audios={audios}
|
||||
locked_audios={locked_audios}
|
||||
locked_images={locked_images}
|
||||
comment={comment}
|
||||
setComment={setData}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
<CommentFormAttaches />
|
||||
|
||||
<Group horizontal className={styles.buttons}>
|
||||
<CommentFormAttachButtons onUpload={onUpload} />
|
||||
<CommentFormFormatButtons element={textarea} handler={onInput} />
|
||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
|
||||
|
||||
<Filler />
|
||||
{isLoading && <LoaderCircle size={20} />}
|
||||
|
||||
{(is_sending_comment || isUploadingNow) && <LoaderCircle size={20} />}
|
||||
|
||||
{id !== 0 && (
|
||||
{isEditing && (
|
||||
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
||||
Отмена
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconRight={id === 0 ? 'enter' : 'check'}
|
||||
disabled={is_sending_comment || isUploadingNow}
|
||||
iconRight={!isEditing ? 'enter' : 'check'}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{id === 0 ? 'Сказать' : 'Сохранить'}
|
||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</CommentFormDropzone>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
</FileUploaderProvider>
|
||||
</FormikProvider>
|
||||
</form>
|
||||
</CommentFormDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
const CommentForm = connect(mapStateToProps, mapDispatchToProps)(CommentFormUnconnected);
|
||||
|
||||
export { CommentForm, CommentFormUnconnected };
|
||||
export { CommentForm };
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
padding: $gap / 2;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
|
||||
.uploads {
|
||||
|
|
|
@ -1,106 +1,83 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import styles from '~/components/comment/CommentForm/styles.module.scss';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||
import { IComment, IFile } from '~/redux/types';
|
||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||
import { IFile } from '~/redux/types';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import assocPath from 'ramda/es/assocPath';
|
||||
import { moveArrItem } from '~/utils/fn';
|
||||
import { useDropZone } from '~/utils/hooks';
|
||||
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
|
||||
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
||||
|
||||
interface IProps {
|
||||
images: IFile[];
|
||||
audios: IFile[];
|
||||
locked_images: IUploadStatus[];
|
||||
locked_audios: IUploadStatus[];
|
||||
comment: IComment;
|
||||
setComment: (data: IComment) => void;
|
||||
onUpload: (files: File[]) => void;
|
||||
}
|
||||
const CommentFormAttaches: FC = () => {
|
||||
const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
|
||||
|
||||
const CommentFormAttaches: FC<IProps> = ({
|
||||
images,
|
||||
audios,
|
||||
locked_images,
|
||||
locked_audios,
|
||||
comment,
|
||||
setComment,
|
||||
onUpload,
|
||||
}) => {
|
||||
const onDrop = useDropZone(onUpload, COMMENT_FILE_TYPES);
|
||||
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||
files,
|
||||
]);
|
||||
|
||||
const hasImageAttaches = images.length > 0 || locked_images.length > 0;
|
||||
const hasAudioAttaches = audios.length > 0 || locked_audios.length > 0;
|
||||
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
|
||||
pending,
|
||||
]);
|
||||
|
||||
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||
files,
|
||||
]);
|
||||
|
||||
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
|
||||
pending,
|
||||
]);
|
||||
|
||||
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
||||
|
||||
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
|
||||
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
|
||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||
|
||||
const onImageMove = useCallback(
|
||||
({ oldIndex, newIndex }: SortEnd) => {
|
||||
setComment(
|
||||
assocPath(
|
||||
['files'],
|
||||
[
|
||||
...audios,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
images.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
],
|
||||
comment
|
||||
)
|
||||
);
|
||||
setFiles([
|
||||
...audios,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
images.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
]);
|
||||
},
|
||||
[images, audios, comment, setComment]
|
||||
);
|
||||
|
||||
const onFileDelete = useCallback(
|
||||
(fileId: IFile['id']) => {
|
||||
setComment(
|
||||
assocPath(
|
||||
['files'],
|
||||
comment.files.filter(file => file.id != fileId),
|
||||
comment
|
||||
)
|
||||
);
|
||||
},
|
||||
[setComment, comment]
|
||||
);
|
||||
|
||||
const onTitleChange = useCallback(
|
||||
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||
setComment(
|
||||
assocPath(
|
||||
['files'],
|
||||
comment.files.map(file =>
|
||||
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||
),
|
||||
comment
|
||||
)
|
||||
);
|
||||
},
|
||||
[comment, setComment]
|
||||
[images, audios, setFiles]
|
||||
);
|
||||
|
||||
const onAudioMove = useCallback(
|
||||
({ oldIndex, newIndex }: SortEnd) => {
|
||||
setComment(
|
||||
assocPath(
|
||||
['files'],
|
||||
[
|
||||
...images,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
audios.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
],
|
||||
comment
|
||||
setFiles([
|
||||
...images,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
audios.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
]);
|
||||
},
|
||||
[images, audios, setFiles]
|
||||
);
|
||||
|
||||
const onFileDelete = useCallback(
|
||||
(fileId: IFile['id']) => {
|
||||
setFiles(files.filter(file => file.id !== fileId));
|
||||
},
|
||||
[setFiles, files]
|
||||
);
|
||||
|
||||
const onAudioTitleChange = useCallback(
|
||||
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||
setFiles(
|
||||
files.map(file =>
|
||||
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||
)
|
||||
);
|
||||
},
|
||||
[images, audios, comment, setComment]
|
||||
[files, setFiles]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -112,7 +89,7 @@ const CommentFormAttaches: FC<IProps> = ({
|
|||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={locked_images}
|
||||
locked={pendingImages}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
|
@ -123,10 +100,10 @@ const CommentFormAttaches: FC<IProps> = ({
|
|||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDelete={onFileDelete}
|
||||
onTitleChange={onTitleChange}
|
||||
onTitleChange={onAudioTitleChange}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={locked_audios}
|
||||
locked={pendingAudios}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.attaches {
|
||||
@include outer_shadow();
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
|
||||
import { FormikProvider } from 'formik';
|
||||
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
|
||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
||||
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
||||
import { LocalCommentFormAttaches } from '~/components/comment/LocalCommentFormAttaches';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { IComment, INode } from '~/redux/types';
|
||||
import { EMPTY_COMMENT } from '~/redux/node/constants';
|
||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||
import styles from './styles.module.scss';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
||||
interface IProps {
|
||||
comment?: IComment;
|
||||
nodeId: INode['id'];
|
||||
onCancelEdit?: () => void;
|
||||
}
|
||||
|
||||
const LocalCommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||
const uploader = useFileUploader(
|
||||
UPLOAD_SUBJECTS.COMMENT,
|
||||
UPLOAD_TARGETS.COMMENTS,
|
||||
comment?.files
|
||||
);
|
||||
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
|
||||
const isLoading = formik.isSubmitting || uploader.isUploading;
|
||||
const isEditing = !!comment?.id;
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
if (formik.status) {
|
||||
formik.setStatus('');
|
||||
}
|
||||
|
||||
if (formik.errors.text) {
|
||||
formik.setErrors({
|
||||
...formik.errors,
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
}, [formik]);
|
||||
|
||||
const error = formik.status || formik.errors.text;
|
||||
|
||||
return (
|
||||
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
||||
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
|
||||
<FormikProvider value={formik}>
|
||||
<FileUploaderProvider value={uploader}>
|
||||
<div className={styles.input}>
|
||||
<LocalCommentFormTextarea setRef={setTextarea} />
|
||||
|
||||
{!!error && (
|
||||
<div className={styles.error} onClick={clearError}>
|
||||
{ERROR_LITERAL[error] || error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCommentFormAttaches />
|
||||
|
||||
<Group horizontal className={styles.buttons}>
|
||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
|
||||
|
||||
{isLoading && <LoaderCircle size={20} />}
|
||||
|
||||
{isEditing && (
|
||||
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
||||
Отмена
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconRight={!isEditing ? 'enter' : 'check'}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||
</Button>
|
||||
</Group>
|
||||
</FileUploaderProvider>
|
||||
</FormikProvider>
|
||||
</form>
|
||||
</CommentFormDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export { LocalCommentForm };
|
|
@ -1,57 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
textarea {
|
||||
min-height: 62px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@include outer_shadow();
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: ($gap / 2) ($gap / 2 + 1px);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@include outer_shadow();
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: transparentize(black, 0.8);
|
||||
padding: $gap / 2;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
|
||||
.uploads {
|
||||
padding: ($gap / 2);
|
||||
display: grid;
|
||||
grid-column-gap: $gap / 2;
|
||||
grid-row-gap: $gap / 2;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
.attaches {
|
||||
@include outer_shadow();
|
||||
}
|
||||
|
||||
.error {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
background: $red;
|
||||
z-index: 10;
|
||||
font: $font_12_regular;
|
||||
box-sizing: border-box;
|
||||
padding: 0 $gap;
|
||||
border-radius: 4px 4px 0 0;
|
||||
transform: translate(-50%, 0);
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import styles from '~/components/comment/CommentForm/styles.module.scss';
|
||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||
import { IFile } from '~/redux/types';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { moveArrItem } from '~/utils/fn';
|
||||
import { useDropZone } from '~/utils/hooks';
|
||||
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
||||
|
||||
const LocalCommentFormAttaches: FC = () => {
|
||||
const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
|
||||
|
||||
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||
files,
|
||||
]);
|
||||
|
||||
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
|
||||
pending,
|
||||
]);
|
||||
|
||||
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||
files,
|
||||
]);
|
||||
|
||||
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
|
||||
pending,
|
||||
]);
|
||||
|
||||
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
||||
|
||||
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
|
||||
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
|
||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||
|
||||
const onImageMove = useCallback(
|
||||
({ oldIndex, newIndex }: SortEnd) => {
|
||||
setFiles([
|
||||
...audios,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
images.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
]);
|
||||
},
|
||||
[images, audios, setFiles]
|
||||
);
|
||||
|
||||
const onAudioMove = useCallback(
|
||||
({ oldIndex, newIndex }: SortEnd) => {
|
||||
setFiles([
|
||||
...images,
|
||||
...(moveArrItem(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
audios.filter(file => !!file)
|
||||
) as IFile[]),
|
||||
]);
|
||||
},
|
||||
[images, audios, setFiles]
|
||||
);
|
||||
|
||||
const onFileDelete = useCallback(
|
||||
(fileId: IFile['id']) => {
|
||||
setFiles(files.filter(file => file.id !== fileId));
|
||||
},
|
||||
[setFiles, files]
|
||||
);
|
||||
|
||||
const onAudioTitleChange = useCallback(
|
||||
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||
setFiles(
|
||||
files.map(file =>
|
||||
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||
)
|
||||
);
|
||||
},
|
||||
[files, setFiles]
|
||||
);
|
||||
|
||||
return (
|
||||
hasAttaches && (
|
||||
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||
{hasImageAttaches && (
|
||||
<SortableImageGrid
|
||||
onDelete={onFileDelete}
|
||||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={pendingImages}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasAudioAttaches && (
|
||||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDelete={onFileDelete}
|
||||
onTitleChange={onAudioTitleChange}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={pendingAudios}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export { LocalCommentFormAttaches };
|
|
@ -1,12 +1,11 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { nodeLockComment } from '~/redux/node/actions';
|
||||
import { IComment } from '~/redux/types';
|
||||
|
||||
interface IProps {
|
||||
id: IComment['id'];
|
||||
onDelete: typeof nodeLockComment;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
}
|
||||
|
||||
const CommendDeleted: FC<IProps> = ({ id, onDelete }) => {
|
||||
|
|
|
@ -2,8 +2,7 @@ import React, { FC } from 'react';
|
|||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||
import { CommentForm } from '../../comment/CommentForm';
|
||||
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
|
||||
import { CommentForm } from '~/components/comment/CommentForm';
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -18,8 +17,7 @@ type IProps = ReturnType<typeof mapStateToProps> & {
|
|||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
|
||||
return (
|
||||
<CommentWrapper user={user}>
|
||||
<CommentForm id={0} is_before={isBefore} />
|
||||
<LocalCommentForm nodeId={nodeId} />
|
||||
<CommentForm nodeId={nodeId} />
|
||||
</CommentWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,89 +1,73 @@
|
|||
import React, { FC, useMemo, memo } from 'react';
|
||||
import React, { FC, memo, useCallback, useMemo } from 'react';
|
||||
import { Comment } from '../../comment/Comment';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { ICommentGroup, IComment } from '~/redux/types';
|
||||
import { IComment, ICommentGroup, IFile } from '~/redux/types';
|
||||
import { groupCommentsByUser } from '~/utils/fn';
|
||||
import { IUser } from '~/redux/auth/types';
|
||||
import { canEditComment } from '~/utils/node';
|
||||
import { nodeLockComment, nodeEditComment, nodeLoadMoreComments } from '~/redux/node/actions';
|
||||
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
||||
import { INodeState } from '~/redux/node/reducer';
|
||||
import { COMMENTS_DISPLAY } from '~/redux/node/constants';
|
||||
import { plural } from '~/utils/dom';
|
||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
interface IProps {
|
||||
comments?: IComment[];
|
||||
comment_data: INodeState['comment_data'];
|
||||
comment_count: INodeState['comment_count'];
|
||||
count: INodeState['comment_count'];
|
||||
user: IUser;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onEdit: typeof nodeEditComment;
|
||||
onLoadMore: typeof nodeLoadMoreComments;
|
||||
order?: 'ASC' | 'DESC';
|
||||
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
||||
}
|
||||
|
||||
const NodeComments: FC<IProps> = memo(
|
||||
({
|
||||
comments,
|
||||
comment_data,
|
||||
user,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onLoadMore,
|
||||
comment_count = 0,
|
||||
order = 'DESC',
|
||||
modalShowPhotoswipe,
|
||||
}) => {
|
||||
const comments_left = useMemo(() => Math.max(0, comment_count - comments.length), [
|
||||
comments,
|
||||
comment_count,
|
||||
]);
|
||||
const NodeComments: FC<IProps> = memo(({ comments, user, count = 0, order = 'DESC' }) => {
|
||||
const dispatch = useDispatch();
|
||||
const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]);
|
||||
|
||||
const groupped: ICommentGroup[] = useMemo(
|
||||
() => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []),
|
||||
[comments, order]
|
||||
);
|
||||
const groupped: ICommentGroup[] = useMemo(
|
||||
() => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []),
|
||||
[comments, order]
|
||||
);
|
||||
|
||||
const more = useMemo(
|
||||
() =>
|
||||
comments_left > 0 && (
|
||||
<div className={styles.more} onClick={onLoadMore}>
|
||||
Показать ещё{' '}
|
||||
{plural(
|
||||
Math.min(comments_left, COMMENTS_DISPLAY),
|
||||
'комментарий',
|
||||
'комментария',
|
||||
'комментариев'
|
||||
)}
|
||||
{comments_left > COMMENTS_DISPLAY ? ` из ${comments_left} оставшихся` : ''}
|
||||
</div>
|
||||
),
|
||||
[comments_left, onLoadMore, COMMENTS_DISPLAY]
|
||||
);
|
||||
const onDelete = useCallback(
|
||||
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)),
|
||||
[dispatch]
|
||||
);
|
||||
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
||||
const onShowPhotoswipe = useCallback(
|
||||
(images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{order === 'DESC' && more}
|
||||
const more = useMemo(
|
||||
() =>
|
||||
left > 0 && (
|
||||
<div className={styles.more} onClick={onLoadMoreComments}>
|
||||
Показать ещё{' '}
|
||||
{plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
|
||||
{left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
|
||||
</div>
|
||||
),
|
||||
[left, onLoadMoreComments]
|
||||
);
|
||||
|
||||
{groupped.map(group => (
|
||||
<Comment
|
||||
key={group.ids.join()}
|
||||
comment_group={group}
|
||||
comment_data={comment_data}
|
||||
can_edit={canEditComment(group, user)}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
||||
/>
|
||||
))}
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{order === 'DESC' && more}
|
||||
|
||||
{order === 'ASC' && more}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
{groupped.map(group => (
|
||||
<Comment
|
||||
key={group.ids.join()}
|
||||
comment_group={group}
|
||||
can_edit={canEditComment(group, user)}
|
||||
onDelete={onDelete}
|
||||
modalShowPhotoswipe={onShowPhotoswipe}
|
||||
/>
|
||||
))}
|
||||
|
||||
{order === 'ASC' && more}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { NodeComments };
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import { selectNode, selectNodeComments } from '~/redux/node/selectors';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { NodeComments } from '~/components/node/NodeComments';
|
||||
import styles from './styles.module.scss';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
@ -10,72 +9,49 @@ import boris from '~/sprites/boris_robot.svg';
|
|||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||
|
||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||
import * as BORIS_ACTIONS from '~/redux/boris/actions';
|
||||
import isBefore from 'date-fns/isBefore';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Footer } from '~/components/main/Footer';
|
||||
import { Sticky } from '~/components/containers/Sticky';
|
||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||
import { BorisStats } from '~/components/boris/BorisStats';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||
import { authSetUser } from '~/redux/auth/actions';
|
||||
import { nodeLoadNode } from '~/redux/node/actions';
|
||||
import { borisLoadStats } from '~/redux/boris/actions';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
node: selectNode(state),
|
||||
user: selectUser(state),
|
||||
stats: selectBorisStats(state),
|
||||
});
|
||||
type IProps = {};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
nodeLoadNode: NODE_ACTIONS.nodeLoadNode,
|
||||
nodeLockComment: NODE_ACTIONS.nodeLockComment,
|
||||
nodeEditComment: NODE_ACTIONS.nodeEditComment,
|
||||
nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
|
||||
authSetUser: AUTH_ACTIONS.authSetUser,
|
||||
modalShowPhotoswipe: MODAL_ACTIONS.modalShowPhotoswipe,
|
||||
borisLoadStats: BORIS_ACTIONS.borisLoadStats,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps &
|
||||
RouteComponentProps<{ id: string }> & {};
|
||||
|
||||
const id = 696;
|
||||
|
||||
const BorisLayoutUnconnected: FC<IProps> = ({
|
||||
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count, id },
|
||||
user,
|
||||
user: { is_user, last_seen_boris },
|
||||
nodeLoadNode,
|
||||
nodeLockComment,
|
||||
nodeEditComment,
|
||||
nodeLoadMoreComments,
|
||||
modalShowPhotoswipe,
|
||||
authSetUser,
|
||||
borisLoadStats,
|
||||
stats,
|
||||
}) => {
|
||||
const BorisLayout: FC<IProps> = () => {
|
||||
const title = useRandomPhrase('BORIS_TITLE');
|
||||
const dispatch = useDispatch();
|
||||
const node = useShallowSelect(selectNode);
|
||||
const user = useShallowSelect(selectUser);
|
||||
const stats = useShallowSelect(selectBorisStats);
|
||||
const comments = useShallowSelect(selectNodeComments);
|
||||
|
||||
useEffect(() => {
|
||||
const last_comment = comments[0];
|
||||
|
||||
if (!last_comment) return;
|
||||
if (last_seen_boris && !isBefore(new Date(last_seen_boris), new Date(last_comment.created_at)))
|
||||
|
||||
if (
|
||||
user.last_seen_boris &&
|
||||
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
|
||||
)
|
||||
return;
|
||||
|
||||
authSetUser({ last_seen_boris: last_comment.created_at });
|
||||
}, [comments, last_seen_boris]);
|
||||
dispatch(authSetUser({ last_seen_boris: last_comment.created_at }));
|
||||
}, [user.last_seen_boris, dispatch, comments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (is_loading) return;
|
||||
nodeLoadNode(id, 'DESC');
|
||||
}, [nodeLoadNode, id]);
|
||||
if (node.is_loading) return;
|
||||
dispatch(nodeLoadNode(696, 'DESC'));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
borisLoadStats();
|
||||
}, [borisLoadStats]);
|
||||
dispatch(borisLoadStats());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
|
@ -92,20 +68,15 @@ const BorisLayoutUnconnected: FC<IProps> = ({
|
|||
<div className={styles.container}>
|
||||
<Card className={styles.content}>
|
||||
<Group className={styles.grid}>
|
||||
{is_user && <NodeCommentForm isBefore nodeId={id} />}
|
||||
{user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />}
|
||||
|
||||
{is_loading_comments ? (
|
||||
{node.is_loading_comments ? (
|
||||
<NodeNoComments is_loading count={7} />
|
||||
) : (
|
||||
<NodeComments
|
||||
comments={comments}
|
||||
comment_data={comment_data}
|
||||
comment_count={comment_count}
|
||||
count={node.comment_count}
|
||||
user={user}
|
||||
onDelete={nodeLockComment}
|
||||
onEdit={nodeEditComment}
|
||||
onLoadMore={nodeLoadMoreComments}
|
||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
||||
order="ASC"
|
||||
/>
|
||||
)}
|
||||
|
@ -139,6 +110,4 @@ const BorisLayoutUnconnected: FC<IProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const BorisLayout = connect(mapStateToProps, mapDispatchToProps)(BorisLayoutUnconnected);
|
||||
|
||||
export { BorisLayout };
|
||||
|
|
|
@ -181,14 +181,9 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
<NodeNoComments is_loading={is_loading_comments || is_loading} />
|
||||
) : (
|
||||
<NodeComments
|
||||
count={comment_count}
|
||||
comments={comments}
|
||||
comment_data={comment_data}
|
||||
comment_count={comment_count}
|
||||
user={user}
|
||||
onDelete={nodeLockComment}
|
||||
onEdit={nodeEditComment}
|
||||
onLoadMore={nodeLoadMoreComments}
|
||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
||||
order="DESC"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -3,11 +3,11 @@ import { RouteComponentProps, useRouteMatch, withRouter } from 'react-router';
|
|||
import styles from './styles.module.scss';
|
||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
import { Grid } from '~/components/containers/Grid';
|
||||
import { CommentForm } from '~/components/comment/CommentForm';
|
||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import { connect } from 'react-redux';
|
||||
import { IUser } from '~/redux/auth/types';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { CommentForm } from '~/components/comment/CommentForm';
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
const mapDispatchToProps = {
|
||||
|
@ -39,7 +39,7 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
|||
|
||||
<Grid className={styles.content}>
|
||||
<div className={styles.comments}>
|
||||
<CommentForm id={0} />
|
||||
<CommentForm nodeId={0} />
|
||||
<NodeNoComments is_loading={false} />
|
||||
</div>
|
||||
</Grid>
|
||||
|
|
|
@ -44,12 +44,6 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({
|
|||
type: NODE_ACTIONS.SET_CURRENT,
|
||||
});
|
||||
|
||||
export const nodePostComment = (id: number, is_before: boolean) => ({
|
||||
id,
|
||||
is_before,
|
||||
type: NODE_ACTIONS.POST_COMMENT,
|
||||
});
|
||||
|
||||
export const nodePostLocalComment = (
|
||||
nodeId: INode['id'],
|
||||
comment: IComment,
|
||||
|
@ -58,7 +52,7 @@ export const nodePostLocalComment = (
|
|||
nodeId,
|
||||
comment,
|
||||
callback,
|
||||
type: NODE_ACTIONS.POST_LOCAL_COMMENT,
|
||||
type: NODE_ACTIONS.POST_COMMENT,
|
||||
});
|
||||
|
||||
export const nodeCancelCommentEdit = (id: number) => ({
|
||||
|
|
|
@ -41,8 +41,7 @@ export const NODE_ACTIONS = {
|
|||
SET_COMMENT_DATA: `${prefix}SET_COMMENT_DATA`,
|
||||
SET_EDITOR: `${prefix}SET_EDITOR`,
|
||||
|
||||
POST_COMMENT: `${prefix}POST_COMMENT`,
|
||||
POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
||||
POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
||||
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
||||
SET_RELATED: `${prefix}SET_RELATED`,
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
nodeLoadNode,
|
||||
nodeLock,
|
||||
nodeLockComment,
|
||||
nodePostComment,
|
||||
nodePostLocalComment,
|
||||
nodeSave,
|
||||
nodeSet,
|
||||
|
@ -25,7 +24,6 @@ import {
|
|||
nodeSetLoadingComments,
|
||||
nodeSetRelated,
|
||||
nodeSetSaveErrors,
|
||||
nodeSetSendingComment,
|
||||
nodeSetTags,
|
||||
nodeUpdateTags
|
||||
} from './actions';
|
||||
|
@ -191,44 +189,7 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
|
|||
return;
|
||||
}
|
||||
|
||||
function* onPostComment({ id }: ReturnType<typeof nodePostComment>) {
|
||||
const { current, comment_data } = yield select(selectNode);
|
||||
|
||||
yield put(nodeSetSendingComment(true));
|
||||
const {
|
||||
data: { comment },
|
||||
error,
|
||||
} = yield call(reqWrapper, postNodeComment, { data: comment_data[id], id: current.id });
|
||||
yield put(nodeSetSendingComment(false));
|
||||
|
||||
if (error || !comment) {
|
||||
return yield put(nodeSetCommentData(id, { error }));
|
||||
}
|
||||
|
||||
const { current: current_node } = yield select(selectNode);
|
||||
|
||||
if (current_node && current_node.id === current.id) {
|
||||
const { comments, comment_data: current_comment_data } = yield select(selectNode);
|
||||
|
||||
if (id === 0) {
|
||||
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
|
||||
yield put(nodeSetComments([comment, ...comments]));
|
||||
} else {
|
||||
yield put(
|
||||
nodeSet({
|
||||
comment_data: omit([id.toString()], current_comment_data),
|
||||
comments: comments.map(item => (item.id === id ? comment : item)),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* onPostLocalComment({
|
||||
nodeId,
|
||||
comment,
|
||||
callback,
|
||||
}: ReturnType<typeof nodePostLocalComment>) {
|
||||
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
|
||||
const { data, error }: Unwrap<ReturnType<typeof postNodeComment>> = yield call(
|
||||
reqWrapper,
|
||||
postNodeComment,
|
||||
|
@ -390,7 +351,6 @@ export default function* nodeSaga() {
|
|||
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);
|
||||
yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostLocalComment);
|
||||
yield takeLatest(NODE_ACTIONS.CANCEL_COMMENT_EDIT, onCancelCommentEdit);
|
||||
yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags);
|
||||
yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga);
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { IState } from '../store';
|
||||
import { INodeState } from './reducer';
|
||||
import { IResultWithStatus, INode } from '../types';
|
||||
|
||||
export const selectNode = (state: IState): INodeState => state.node;
|
||||
|
||||
// export const catchNodeErrors = (data: IResultWithStatus<INode>): IResultWithStatus<INode> => ({
|
||||
// data,
|
||||
// errors: data.errors,
|
||||
// })
|
||||
export const selectNode = (state: IState) => state.node;
|
||||
export const selectNodeComments = (state: IState) => state.node.comments;
|
||||
export const selectNodeCurrent = (state: IState) => state.node.current;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue