mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
refactor node comments container
This commit is contained in:
parent
eea7095e65
commit
34797c2ac0
32 changed files with 9 additions and 9 deletions
|
@ -1,45 +0,0 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ButtonGroup } from '~/components/input/ButtonGroup';
|
||||
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||
|
||||
interface IProps {
|
||||
onUpload: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const CommentFormAttachButtons: FC<IProps> = ({ onUpload }) => {
|
||||
const onInputChange = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const files = Array.from(event.target?.files as File[]).filter(
|
||||
(file: File) => COMMENT_FILE_TYPES.includes(file.type),
|
||||
);
|
||||
if (!files || !files.length) return;
|
||||
|
||||
onUpload(files);
|
||||
},
|
||||
[onUpload],
|
||||
);
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button iconLeft="photo" size="small" color="gray" iconOnly type="button">
|
||||
<input type="file" onInput={onInputChange} multiple accept="image/*" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
iconRight="audio"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
>
|
||||
<input type="file" onInput={onInputChange} multiple accept="audio/*" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommentFormAttachButtons };
|
|
@ -1,90 +0,0 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { SortableAudioGrid } from '~/components/sortable/SortableAudioGrid';
|
||||
import { SortableImageGrid } from '~/components/sortable/SortableImageGrid';
|
||||
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||
import { useFileDropZone } from '~/hooks';
|
||||
import { IFile } from '~/types';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const CommentFormAttaches: FC = () => {
|
||||
const {
|
||||
files,
|
||||
pendingImages,
|
||||
pendingAudios,
|
||||
filesAudios,
|
||||
filesImages,
|
||||
uploadFiles,
|
||||
setFiles,
|
||||
} = useUploaderContext();
|
||||
|
||||
const onDrop = useFileDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
||||
|
||||
const hasImageAttaches = filesImages.length > 0 || pendingImages.length > 0;
|
||||
const hasAudioAttaches = filesAudios.length > 0 || pendingAudios.length > 0;
|
||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||
|
||||
const onImageMove = useCallback(
|
||||
(newFiles: IFile[]) => {
|
||||
setFiles([...filesAudios, ...newFiles.filter((it) => it)]);
|
||||
},
|
||||
[setFiles, filesAudios],
|
||||
);
|
||||
|
||||
const onAudioMove = useCallback(
|
||||
(newFiles: IFile[]) => {
|
||||
setFiles([...filesImages, ...newFiles]);
|
||||
},
|
||||
[setFiles, filesImages],
|
||||
);
|
||||
|
||||
const onFileDelete = useCallback(
|
||||
(fileId: IFile['id']) => {
|
||||
setFiles(files.filter((file) => file.id !== fileId));
|
||||
},
|
||||
[files, setFiles],
|
||||
);
|
||||
|
||||
const onAudioTitleChange = useCallback(
|
||||
(fileId: IFile['id'], title: string) => {
|
||||
setFiles(
|
||||
files.map((file) =>
|
||||
file.id === fileId
|
||||
? { ...file, metadata: { ...file.metadata, title } }
|
||||
: file,
|
||||
),
|
||||
);
|
||||
},
|
||||
[files, setFiles],
|
||||
);
|
||||
|
||||
if (!hasAttaches) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||
{hasImageAttaches && (
|
||||
<SortableImageGrid
|
||||
onDelete={onFileDelete}
|
||||
onSortEnd={onImageMove}
|
||||
items={filesImages}
|
||||
locked={pendingImages}
|
||||
size={160}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasAudioAttaches && (
|
||||
<SortableAudioGrid
|
||||
items={filesAudios}
|
||||
onDelete={onFileDelete}
|
||||
onTitleChange={onAudioTitleChange}
|
||||
onSortEnd={onAudioMove}
|
||||
locked={pendingAudios}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommentFormAttaches };
|
|
@ -1,5 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.attaches {
|
||||
@include outer_shadow();
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import React, { FC, useCallback, useEffect } from 'react';
|
||||
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ButtonGroup } from '~/components/input/ButtonGroup';
|
||||
import { useFormatWrapper, wrapTextInsideInput } from '~/hooks/dom/useFormatWrapper';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
element: HTMLTextAreaElement;
|
||||
handler: (val: string) => void;
|
||||
}
|
||||
|
||||
const CommentFormFormatButtons: FC<IProps> = ({ element, handler }) => {
|
||||
const wrapper = useFormatWrapper(handler);
|
||||
|
||||
const wrap = useCallback((prefix = '', suffix = '') => wrapper(element, prefix, suffix), [
|
||||
element,
|
||||
wrapper,
|
||||
]);
|
||||
|
||||
const wrapBold = useCallback(
|
||||
event => {
|
||||
event.preventDefault();
|
||||
wrapTextInsideInput(element, '**', '**', handler);
|
||||
},
|
||||
[element, handler]
|
||||
);
|
||||
|
||||
const wrapItalic = useCallback(
|
||||
event => {
|
||||
event.preventDefault();
|
||||
wrapTextInsideInput(element, '*', '*', handler);
|
||||
},
|
||||
[element, handler]
|
||||
);
|
||||
|
||||
const onKeyPress = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (!event.ctrlKey) return;
|
||||
|
||||
if (event.code === 'KeyB') {
|
||||
wrapBold(event);
|
||||
}
|
||||
|
||||
if (event.code === 'KeyI') {
|
||||
wrapItalic(event);
|
||||
}
|
||||
},
|
||||
[wrapBold, wrapItalic]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.addEventListener('keypress', onKeyPress);
|
||||
|
||||
return () => element.removeEventListener('keypress', onKeyPress);
|
||||
}, [element, onKeyPress]);
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.wrap}>
|
||||
<Button
|
||||
onClick={wrapBold}
|
||||
iconLeft="bold"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
label="Жирный Ctrl+B"
|
||||
className={styles.button}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={wrap('*', '*')}
|
||||
iconLeft="italic"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
label="Наклонный Ctrl+I"
|
||||
className={styles.button}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={wrap('## ', '')}
|
||||
iconLeft="title"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
label="Заголовок"
|
||||
className={styles.button}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={wrap('[ссылка](', ')')}
|
||||
iconLeft="link"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
label="Ссылка"
|
||||
className={styles.button}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={wrap('// ')}
|
||||
size="small"
|
||||
color="gray"
|
||||
iconOnly
|
||||
type="button"
|
||||
label="Коммент"
|
||||
className={styles.button}
|
||||
>
|
||||
{'/ /'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommentFormFormatButtons };
|
|
@ -1,14 +0,0 @@
|
|||
@import '~/styles/variables.scss';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import React, {
|
||||
forwardRef,
|
||||
KeyboardEventHandler,
|
||||
TextareaHTMLAttributes,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { useCommentFormContext } from '~/hooks/comments/useCommentFormFormik';
|
||||
|
||||
interface IProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const CommentFormTextarea = forwardRef<HTMLTextAreaElement, IProps>(
|
||||
({ ...rest }, ref) => {
|
||||
const { values, handleChange, handleSubmit, isSubmitting } =
|
||||
useCommentFormContext();
|
||||
|
||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||
({ ctrlKey, key, metaKey }) => {
|
||||
if ((ctrlKey || metaKey) && key === 'Enter') handleSubmit(undefined);
|
||||
},
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
const placeholder = useRandomPhrase('SIMPLE');
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...rest}
|
||||
ref={ref}
|
||||
value={values.text}
|
||||
handler={handleChange('text')}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={isSubmitting}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { CommentFormTextarea };
|
|
@ -1,123 +0,0 @@
|
|||
import { FC, useCallback, useState } from 'react';
|
||||
|
||||
import { FormikProvider } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { EMPTY_COMMENT } from '~/constants/node';
|
||||
import { useCommentFormFormik } from '~/hooks/comments/useCommentFormFormik';
|
||||
import { useInputPasteUpload } from '~/hooks/dom/useInputPasteUpload';
|
||||
import { IComment } from '~/types';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
|
||||
import { CommentFormAttachButtons } from './components/CommentFormAttachButtons';
|
||||
import { CommentFormAttaches } from './components/CommentFormAttaches';
|
||||
import { CommentFormFormatButtons } from './components/CommentFormFormatButtons';
|
||||
import { CommentFormTextarea } from './components/CommentFormTextarea';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
comment?: IComment;
|
||||
allowUploads?: boolean;
|
||||
|
||||
saveComment: (data: IComment) => Promise<IComment | undefined>;
|
||||
onCancelEdit?: () => void;
|
||||
}
|
||||
|
||||
const CommentForm: FC<IProps> = observer(
|
||||
({ comment, allowUploads, saveComment, onCancelEdit }) => {
|
||||
const [textarea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
|
||||
const uploader = useUploaderContext();
|
||||
|
||||
const formik = useCommentFormFormik(
|
||||
comment || EMPTY_COMMENT,
|
||||
uploader.files,
|
||||
uploader.setFiles,
|
||||
saveComment,
|
||||
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;
|
||||
const onPaste = useInputPasteUpload(uploader.uploadFiles);
|
||||
|
||||
return (
|
||||
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
|
||||
<FormikProvider value={formik}>
|
||||
<div className={styles.input}>
|
||||
<CommentFormTextarea onPaste={onPaste} ref={setTextArea} />
|
||||
|
||||
{!!error && (
|
||||
<div className={styles.error} onClick={clearError}>
|
||||
{ERROR_LITERAL[error] || error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{allowUploads && <CommentFormAttaches />}
|
||||
|
||||
<div className={styles.buttons}>
|
||||
{allowUploads && (
|
||||
<div className={styles.button_column}>
|
||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.button_column}>
|
||||
{!!textarea && (
|
||||
<CommentFormFormatButtons
|
||||
element={textarea}
|
||||
handler={formik.handleChange('text')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Filler />
|
||||
|
||||
<div className={styles.button_column}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
size="small"
|
||||
color="link"
|
||||
type="button"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
color="gray"
|
||||
iconRight={!isEditing ? 'enter' : 'check'}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormikProvider>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { CommentForm };
|
|
@ -1,60 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
textarea {
|
||||
min-height: 62px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@include row_shadow;
|
||||
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
background: $content_bg_dark;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
flex-wrap: wrap;
|
||||
padding: $gap * 0.25;
|
||||
}
|
||||
|
||||
.button_column {
|
||||
padding: $gap * 0.25;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.uploads {
|
||||
padding: ($gap * 0.5);
|
||||
display: grid;
|
||||
grid-column-gap: $gap * 0.5;
|
||||
grid-row-gap: $gap * 0.5;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
.attaches {
|
||||
@include outer_shadow();
|
||||
}
|
||||
|
||||
.error {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
background: $color_danger;
|
||||
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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue