1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

#34 made local comment form uploads

This commit is contained in:
Fedor Katurov 2021-02-27 17:51:12 +07:00
parent f45e34f330
commit 051b199d5d
14 changed files with 422 additions and 189 deletions

View file

@ -5,7 +5,6 @@ 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 { CommentForm } from '../CommentForm';
import { CommendDeleted } from '../../node/CommendDeleted';
import * as MODAL_ACTIONS from '~/redux/modal/actions';
@ -50,17 +49,12 @@ const Comment: FC<IProps> = memo(
return <CommendDeleted id={comment.id} onDelete={onDelete} key={comment.id} />;
}
if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) {
return <CommentForm id={comment.id} key={comment.id} />;
}
return (
<CommentContent
comment={comment}
key={comment.id}
can_edit={!!can_edit}
onDelete={onDelete}
onEdit={onEdit}
modalShowPhotoswipe={modalShowPhotoswipe}
/>
);

View file

@ -1,112 +1,116 @@
import React, { FC, useMemo, memo, createElement, useCallback, Fragment } from 'react';
import React, { createElement, FC, Fragment, memo, useCallback, useMemo, useState } from 'react';
import { IComment, IFile } from '~/redux/types';
import { path } from 'ramda';
import { formatCommentText, getURL, getPrettyDate } from '~/utils/dom';
import { append, assocPath, path } from 'ramda';
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
import { Group } from '~/components/containers/Group';
import styles from './styles.module.scss';
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
import { assocPath } from 'ramda';
import { append } from 'ramda';
import reduce from 'ramda/es/reduce';
import { AudioPlayer } from '~/components/media/AudioPlayer';
import classnames from 'classnames';
import { PRESETS } from '~/constants/urls';
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
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 { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectNode } from '~/redux/node/selectors';
interface IProps {
comment: IComment;
can_edit: boolean;
onDelete: typeof nodeLockComment;
onEdit: typeof nodeEditComment;
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
}
const CommentContent: FC<IProps> = memo(
({ comment, can_edit, onDelete, onEdit, modalShowPhotoswipe }) => {
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
() =>
reduce(
(group, file) => assocPath([file.type], append(file, group[file.type]), group),
{},
comment.files
),
[comment]
);
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalShowPhotoswipe }) => {
const [isEditing, setIsEditing] = useState(false);
const { current } = useShallowSelect(selectNode);
const onLockClick = useCallback(() => {
onDelete(comment.id, !comment.deleted_at);
}, [comment, onDelete]);
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
const onEditClick = useCallback(() => {
onEdit(comment.id);
}, [comment, onEdit]);
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
() =>
reduce(
(group, file) => assocPath([file.type], append(file, group[file.type]), group),
{},
comment.files
),
[comment]
);
const menu = useMemo(
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />,
[can_edit, comment, onEditClick, onLockClick]
);
const onLockClick = useCallback(() => {
onDelete(comment.id, !comment.deleted_at);
}, [comment, onDelete]);
const blocks = useMemo(
() =>
!!comment.text.trim()
? formatCommentText(path(['user', 'username'], comment), comment.text)
: [],
[comment.text]
);
const menu = useMemo(
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
[can_edit, startEditing, onLockClick]
);
return (
<div className={styles.wrap}>
{comment.text && (
<Group className={classnames(styles.block, styles.block_text)}>
{menu}
const blocks = useMemo(
() =>
!!comment.text.trim()
? formatCommentText(path(['user', 'username'], comment), comment.text)
: [],
[comment]
);
<Group className={styles.renderers}>
{blocks.map(
(block, key) =>
COMMENT_BLOCK_RENDERERS[block.type] &&
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
)}
</Group>
if (isEditing) {
return <LocalCommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
}
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
return (
<div className={styles.wrap}>
{comment.text && (
<Group className={classnames(styles.block, styles.block_text)}>
{menu}
<Group className={styles.renderers}>
{blocks.map(
(block, key) =>
COMMENT_BLOCK_RENDERERS[block.type] &&
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
)}
</Group>
)}
{groupped.image && groupped.image.length > 0 && (
<div className={classnames(styles.block, styles.block_image)}>
{menu}
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
</Group>
)}
<div className={styles.images}>
{groupped.image.map((file, index) => (
<div key={file.id} onClick={() => modalShowPhotoswipe(groupped.image, index)}>
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
</div>
))}
</div>
{groupped.image && groupped.image.length > 0 && (
<div className={classnames(styles.block, styles.block_image)}>
{menu}
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
</div>
)}
{groupped.audio && groupped.audio.length > 0 && (
<Fragment>
{groupped.audio.map(file => (
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
{menu}
<AudioPlayer file={file} />
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
<div className={styles.images}>
{groupped.image.map((file, index) => (
<div key={file.id} onClick={() => modalShowPhotoswipe(groupped.image, index)}>
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
</div>
))}
</Fragment>
)}
</div>
);
}
);
</div>
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
</div>
)}
{groupped.audio && groupped.audio.length > 0 && (
<Fragment>
{groupped.audio.map(file => (
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
{menu}
<AudioPlayer file={file} />
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
</div>
))}
</Fragment>
)}
</div>
);
});
export { CommentContent };

View file

@ -1,12 +1,4 @@
import React, {
FC,
KeyboardEventHandler,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
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';
@ -30,7 +22,6 @@ import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
const mapStateToProps = (state: IState) => ({
node: selectNode(state),
@ -237,8 +228,6 @@ const CommentFormUnconnected: FC<IProps> = memo(
</Group>
</form>
</CommentFormDropzone>
<LocalCommentForm />
</>
);
}

View file

@ -1,30 +1,64 @@
import React, { FC, useState } from 'react';
import { CommentFormValues, useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
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';
interface IProps {}
interface IProps {
comment?: IComment;
nodeId: INode['id'];
isBefore?: boolean;
onCancelEdit?: () => void;
}
const initialValues: CommentFormValues = {
text: '',
images: [],
songs: [],
};
const LocalCommentForm: FC<IProps> = () => {
const LocalCommentForm: FC<IProps> = ({ comment, nodeId, isBefore, onCancelEdit }) => {
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
const { formik } = useCommentFormFormik(initialValues);
const uploader = useFileUploader(UPLOAD_SUBJECTS.COMMENT, UPLOAD_TARGETS.COMMENTS);
const formik = useCommentFormFormik(
comment || EMPTY_COMMENT,
nodeId,
uploader,
onCancelEdit,
isBefore
);
const isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id;
return (
<form onSubmit={formik.handleSubmit}>
<FormikProvider value={formik}>
<LocalCommentFormTextarea setRef={setTextarea} />
{formik.isSubmitting && <div>LOADING</div>}
{!!formik.status && <div>error: {formik.status}</div>}
<Button size="small" disabled={formik.isSubmitting}>
SEND
</Button>
<FileUploaderProvider value={uploader}>
<LocalCommentFormTextarea setRef={setTextarea} />
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
<LocalCommentFormAttaches />
{isLoading && <LoaderCircle size={20} />}
{isEditing && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
Отмена
</Button>
)}
<Button
size="small"
color="gray"
iconRight={!isEditing ? 'enter' : 'check'}
disabled={isLoading}
>
{!isEditing ? 'Сказать' : 'Сохранить'}
</Button>
</FileUploaderProvider>
</FormikProvider>
</form>
);

View file

@ -0,0 +1,116 @@
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 };