1
0
Fork 0
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:
Fedor Katurov 2023-11-19 18:19:03 +06:00
parent eea7095e65
commit 34797c2ac0
32 changed files with 9 additions and 9 deletions

View file

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

View file

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

View file

@ -1,5 +0,0 @@
@import "src/styles/variables";
.attaches {
@include outer_shadow();
}

View file

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

View file

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

View file

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

View file

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

View file

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