mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
removed upload redux store
This commit is contained in:
parent
140e36b6b7
commit
95b92b643f
38 changed files with 398 additions and 691 deletions
|
@ -1,12 +1,13 @@
|
||||||
import { api, cleanResult } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
|
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { ApiUploadFileRequest, ApiUploadFIleResult } from '~/redux/uploads/types';
|
import { ApiUploadFileRequest, ApiUploadFIleResult } from '~/api/uploads/types';
|
||||||
|
import { UploadTarget, UploadType } from '~/constants/uploads';
|
||||||
|
|
||||||
export const apiUploadFile = ({
|
export const apiUploadFile = ({
|
||||||
file,
|
file,
|
||||||
target = 'others',
|
target = UploadTarget.Others,
|
||||||
type = 'image',
|
type = UploadType.Image,
|
||||||
onProgress,
|
onProgress,
|
||||||
}: ApiUploadFileRequest) => {
|
}: ApiUploadFileRequest) => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
11
src/api/uploads/types.ts
Normal file
11
src/api/uploads/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { IFile, IUploadProgressHandler } from '~/redux/types';
|
||||||
|
import { UploadTarget, UploadType } from '~/constants/uploads';
|
||||||
|
|
||||||
|
export type ApiUploadFileRequest = {
|
||||||
|
file: File;
|
||||||
|
type: UploadType;
|
||||||
|
target: UploadTarget;
|
||||||
|
onProgress: IUploadProgressHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUploadFIleResult = IFile;
|
|
@ -4,7 +4,7 @@ import { append, assocPath, path } from 'ramda';
|
||||||
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import reduce from 'ramda/es/reduce';
|
import reduce from 'ramda/es/reduce';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -30,12 +30,12 @@ const CommentContent: FC<IProps> = memo(
|
||||||
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||||
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
|
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
|
||||||
|
|
||||||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
const groupped = useMemo<Record<UploadType, IFile[]>>(
|
||||||
() =>
|
() =>
|
||||||
reduce(
|
reduce(
|
||||||
(group, file) =>
|
(group, file) =>
|
||||||
file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
|
file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
|
||||||
{},
|
{} as Record<UploadType, IFile[]>,
|
||||||
comment.files
|
comment.files
|
||||||
),
|
),
|
||||||
[comment]
|
[comment]
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { useCommentFormFormik } from '~/hooks/comments/useCommentFormFormik';
|
||||||
import { FormikProvider } from 'formik';
|
import { FormikProvider } from 'formik';
|
||||||
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { FileUploaderProvider, useFileUploader } from '~/hooks/data/useFileUploader';
|
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
|
||||||
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
||||||
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
||||||
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
|
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
|
||||||
|
@ -16,6 +15,9 @@ import styles from './styles.module.scss';
|
||||||
import { ERROR_LITERAL } from '~/constants/errors';
|
import { ERROR_LITERAL } from '~/constants/errors';
|
||||||
import { useInputPasteUpload } from '~/hooks/dom/useInputPasteUpload';
|
import { useInputPasteUpload } from '~/hooks/dom/useInputPasteUpload';
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
|
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comment?: IComment;
|
comment?: IComment;
|
||||||
|
@ -24,13 +26,9 @@ interface IProps {
|
||||||
onCancelEdit?: () => void;
|
onCancelEdit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit }) => {
|
const CommentForm: FC<IProps> = observer(({ comment, nodeId, saveComment, onCancelEdit }) => {
|
||||||
const [textarea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
|
const [textarea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
|
||||||
const uploader = useFileUploader(
|
const uploader = useUploader(UploadSubject.Comment, UploadTarget.Comments, comment?.files);
|
||||||
UPLOAD_SUBJECTS.COMMENT,
|
|
||||||
UPLOAD_TARGETS.COMMENTS,
|
|
||||||
comment?.files
|
|
||||||
);
|
|
||||||
const formik = useCommentFormFormik(
|
const formik = useCommentFormFormik(
|
||||||
comment || EMPTY_COMMENT,
|
comment || EMPTY_COMMENT,
|
||||||
nodeId,
|
nodeId,
|
||||||
|
@ -61,7 +59,7 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit })
|
||||||
<UploadDropzone onUpload={uploader.uploadFiles}>
|
<UploadDropzone onUpload={uploader.uploadFiles}>
|
||||||
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
|
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
|
||||||
<FormikProvider value={formik}>
|
<FormikProvider value={formik}>
|
||||||
<FileUploaderProvider value={uploader}>
|
<UploaderContextProvider value={uploader}>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
<LocalCommentFormTextarea onPaste={onPaste} ref={setTextArea} />
|
<LocalCommentFormTextarea onPaste={onPaste} ref={setTextArea} />
|
||||||
|
|
||||||
|
@ -110,11 +108,11 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit })
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FileUploaderProvider>
|
</UploaderContextProvider>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</form>
|
</form>
|
||||||
</UploadDropzone>
|
</UploadDropzone>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export { CommentForm };
|
export { CommentForm };
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { ButtonGroup } from '~/components/input/ButtonGroup';
|
import { ButtonGroup } from '~/components/input/ButtonGroup';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
|
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onUpload: (files: File[]) => void;
|
onUpload: (files: File[]) => void;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
||||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||||
|
@ -6,68 +6,59 @@ import { IFile } from '~/redux/types';
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
import { moveArrItem } from '~/utils/fn';
|
import { moveArrItem } from '~/utils/fn';
|
||||||
import { useFileDropZone } from '~/hooks';
|
import { useFileDropZone } from '~/hooks';
|
||||||
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||||
import { useFileUploaderContext } from '~/hooks/data/useFileUploader';
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
|
|
||||||
const CommentFormAttaches: FC = () => {
|
const CommentFormAttaches: FC = () => {
|
||||||
const uploader = useFileUploaderContext();
|
const {
|
||||||
const { files, pending, setFiles, uploadFiles } = uploader!;
|
|
||||||
|
|
||||||
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
|
||||||
files,
|
files,
|
||||||
]);
|
pendingImages,
|
||||||
|
pendingAudios,
|
||||||
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
|
filesAudios,
|
||||||
pending,
|
filesImages,
|
||||||
]);
|
uploadFiles,
|
||||||
|
setFiles,
|
||||||
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
} = useUploaderContext();
|
||||||
files,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
|
|
||||||
pending,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onDrop = useFileDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
const onDrop = useFileDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
||||||
|
|
||||||
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
|
const hasImageAttaches = filesImages.length > 0 || pendingImages.length > 0;
|
||||||
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
|
const hasAudioAttaches = filesAudios.length > 0 || pendingAudios.length > 0;
|
||||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||||
|
|
||||||
const onImageMove = useCallback(
|
const onImageMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
({ oldIndex, newIndex }: SortEnd) => {
|
||||||
setFiles([
|
setFiles([
|
||||||
...audios,
|
...filesAudios,
|
||||||
...(moveArrItem(
|
...(moveArrItem(
|
||||||
oldIndex,
|
oldIndex,
|
||||||
newIndex,
|
newIndex,
|
||||||
images.filter(file => !!file)
|
filesImages.filter(file => !!file)
|
||||||
) as IFile[]),
|
) as IFile[]),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[images, audios, setFiles]
|
[setFiles, filesImages, filesAudios]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAudioMove = useCallback(
|
const onAudioMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
({ oldIndex, newIndex }: SortEnd) => {
|
||||||
setFiles([
|
setFiles([
|
||||||
...images,
|
...filesImages,
|
||||||
...(moveArrItem(
|
...(moveArrItem(
|
||||||
oldIndex,
|
oldIndex,
|
||||||
newIndex,
|
newIndex,
|
||||||
audios.filter(file => !!file)
|
filesAudios.filter(file => !!file)
|
||||||
) as IFile[]),
|
) as IFile[]),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[images, audios, setFiles]
|
[setFiles, filesImages, filesAudios]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFileDelete = useCallback(
|
const onFileDelete = useCallback(
|
||||||
(fileId: IFile['id']) => {
|
(fileId: IFile['id']) => {
|
||||||
setFiles(files.filter(file => file.id !== fileId));
|
setFiles(files.filter(file => file.id !== fileId));
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[files, setFiles]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAudioTitleChange = useCallback(
|
const onAudioTitleChange = useCallback(
|
||||||
|
@ -90,7 +81,7 @@ const CommentFormAttaches: FC = () => {
|
||||||
onDelete={onFileDelete}
|
onDelete={onFileDelete}
|
||||||
onSortEnd={onImageMove}
|
onSortEnd={onImageMove}
|
||||||
axis="xy"
|
axis="xy"
|
||||||
items={images}
|
items={filesImages}
|
||||||
locked={pendingImages}
|
locked={pendingImages}
|
||||||
pressDelay={50}
|
pressDelay={50}
|
||||||
helperClass={styles.helper}
|
helperClass={styles.helper}
|
||||||
|
@ -100,7 +91,7 @@ const CommentFormAttaches: FC = () => {
|
||||||
|
|
||||||
{hasAudioAttaches && (
|
{hasAudioAttaches && (
|
||||||
<SortableAudioGrid
|
<SortableAudioGrid
|
||||||
items={audios}
|
items={filesAudios}
|
||||||
onDelete={onFileDelete}
|
onDelete={onFileDelete}
|
||||||
onTitleChange={onAudioTitleChange}
|
onTitleChange={onAudioTitleChange}
|
||||||
onSortEnd={onAudioMove}
|
onSortEnd={onAudioMove}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { ImageGrid } from '../ImageGrid';
|
import { ImageGrid } from '../ImageGrid';
|
||||||
import { AudioGrid } from '../AudioGrid';
|
import { AudioGrid } from '../AudioGrid';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -7,25 +7,28 @@ import { NodeEditorProps } from '~/types/node';
|
||||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
||||||
import { useNodeAudios } from '~/hooks/node/useNodeAudios';
|
import { useNodeAudios } from '~/hooks/node/useNodeAudios';
|
||||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||||
import { useFileUploaderContext } from '~/hooks/data/useFileUploader';
|
|
||||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||||
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
|
import { values } from 'ramda';
|
||||||
|
|
||||||
type IProps = NodeEditorProps;
|
type IProps = NodeEditorProps;
|
||||||
|
|
||||||
const AudioEditor: FC<IProps> = () => {
|
const AudioEditor: FC<IProps> = () => {
|
||||||
const { values } = useNodeFormContext();
|
const formik = useNodeFormContext();
|
||||||
const { pending, setFiles, uploadFiles } = useFileUploaderContext()!;
|
const { pending, setFiles, uploadFiles } = useUploaderContext()!;
|
||||||
|
|
||||||
const images = useNodeImages(values);
|
const images = useNodeImages(formik.values);
|
||||||
const audios = useNodeAudios(values);
|
const audios = useNodeAudios(formik.values);
|
||||||
|
|
||||||
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
|
const pendingImages = useMemo(
|
||||||
pending,
|
() => values(pending).filter(item => item.type === UploadType.Image),
|
||||||
]);
|
[pending]
|
||||||
|
);
|
||||||
|
|
||||||
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
|
const pendingAudios = useMemo(
|
||||||
pending,
|
() => values(pending).filter(item => item.type === UploadType.Audio),
|
||||||
]);
|
[pending]
|
||||||
|
);
|
||||||
|
|
||||||
const setImages = useCallback(values => setFiles([...values, ...audios]), [setFiles, audios]);
|
const setImages = useCallback(values => setFiles([...values, ...audios]), [setFiles, audios]);
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
import { IFile } from '~/redux/types';
|
import { IFile } from '~/redux/types';
|
||||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
|
||||||
import { moveArrItem } from '~/utils/fn';
|
import { moveArrItem } from '~/utils/fn';
|
||||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
setFiles: (val: IFile[]) => void;
|
setFiles: (val: IFile[]) => void;
|
||||||
locked: IUploadStatus[];
|
locked: UploadStatus[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { IEditorComponentProps } from '~/types/node';
|
import { IEditorComponentProps } from '~/types/node';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {};
|
type IProps = IEditorComponentProps & {};
|
||||||
|
@ -9,7 +9,7 @@ const EditorAudioUploadButton: FC<IProps> = () => (
|
||||||
<EditorUploadButton
|
<EditorUploadButton
|
||||||
accept="audio/*"
|
accept="audio/*"
|
||||||
icon="audio"
|
icon="audio"
|
||||||
type={UPLOAD_TYPES.AUDIO}
|
type={UploadType.Audio}
|
||||||
label="Добавить аудио"
|
label="Добавить аудио"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { IEditorComponentProps } from '~/types/node';
|
import { IEditorComponentProps } from '~/types/node';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {};
|
type IProps = IEditorComponentProps & {};
|
||||||
|
@ -9,7 +9,7 @@ const EditorImageUploadButton: FC<IProps> = () => (
|
||||||
<EditorUploadButton
|
<EditorUploadButton
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
icon="image"
|
icon="image"
|
||||||
type={UPLOAD_TYPES.IMAGE}
|
type={UploadType.Image}
|
||||||
label="Добавить фоточек"
|
label="Добавить фоточек"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import React, { ChangeEvent, FC, useCallback } from 'react';
|
import React, { ChangeEvent, FC, useCallback } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { IEditorComponentProps } from '~/types/node';
|
import { IEditorComponentProps } from '~/types/node';
|
||||||
import { useFileUploaderContext } from '~/hooks/data/useFileUploader';
|
|
||||||
import { getFileType } from '~/utils/uploader';
|
import { getFileType } from '~/utils/uploader';
|
||||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {
|
type IProps = IEditorComponentProps & {
|
||||||
accept?: string;
|
accept?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
type?: typeof UPLOAD_TYPES[keyof typeof UPLOAD_TYPES];
|
type?: UploadType;
|
||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditorUploadButton: FC<IProps> = ({
|
const EditorUploadButton: FC<IProps> = ({
|
||||||
accept = 'image/*',
|
accept = 'image/*',
|
||||||
icon = 'plus',
|
icon = 'plus',
|
||||||
type = UPLOAD_TYPES.IMAGE,
|
type = UploadType.Image,
|
||||||
label,
|
label,
|
||||||
}) => {
|
}) => {
|
||||||
const { uploadFiles } = useFileUploaderContext()!;
|
const { uploadFiles } = useUploaderContext()!;
|
||||||
const { values } = useNodeFormContext();
|
const { values } = useNodeFormContext();
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import React, { ChangeEvent, FC, useCallback, useEffect } from 'react';
|
import React, { ChangeEvent, FC, useCallback, useEffect } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadSubject, UploadTarget, UploadType } from '~/constants/uploads';
|
||||||
import { path } from 'ramda';
|
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { IEditorComponentProps } from '~/types/node';
|
import { IEditorComponentProps } from '~/types/node';
|
||||||
import { useFileUploader } from '~/hooks/data/useFileUploader';
|
|
||||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||||
import { getFileType } from '~/utils/uploader';
|
import { getFileType } from '~/utils/uploader';
|
||||||
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {};
|
type IProps = IEditorComponentProps & {};
|
||||||
|
|
||||||
const EditorUploadCoverButton: FC<IProps> = ({}) => {
|
const EditorUploadCoverButton: FC<IProps> = ({}) => {
|
||||||
const { values, setFieldValue } = useNodeFormContext();
|
const { values, setFieldValue } = useNodeFormContext();
|
||||||
const { uploadFiles, files } = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, []);
|
const { uploadFiles, files, pendingImages } = useUploader(
|
||||||
|
UploadSubject.Editor,
|
||||||
|
UploadTarget.Nodes,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const background = values.cover ? getURL(values.cover, PRESETS['300']) : null;
|
const background = values.cover ? getURL(values.cover, PRESETS['300']) : null;
|
||||||
const preview = status && path(['preview'], status);
|
const preview = pendingImages?.[0]?.thumbnail || '';
|
||||||
|
|
||||||
const onDropCover = useCallback(() => {
|
const onDropCover = useCallback(() => {
|
||||||
setFieldValue('cover', null);
|
setFieldValue('cover', null);
|
||||||
|
@ -26,7 +29,7 @@ const EditorUploadCoverButton: FC<IProps> = ({}) => {
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(event.target.files || [])
|
const files = Array.from(event.target.files || [])
|
||||||
.filter(file => getFileType(file) === UPLOAD_TYPES.IMAGE)
|
.filter(file => getFileType(file) === UploadType.Image)
|
||||||
.slice(0, 1);
|
.slice(0, 1);
|
||||||
|
|
||||||
uploadFiles(files);
|
uploadFiles(files);
|
||||||
|
|
|
@ -2,18 +2,19 @@ import React, { FC } from 'react';
|
||||||
import { ImageGrid } from '~/components/editors/ImageGrid';
|
import { ImageGrid } from '~/components/editors/ImageGrid';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { NodeEditorProps } from '~/types/node';
|
import { NodeEditorProps } from '~/types/node';
|
||||||
import { useFileUploaderContext } from '~/hooks/data/useFileUploader';
|
|
||||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||||
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
|
import { values } from 'ramda';
|
||||||
|
|
||||||
type IProps = NodeEditorProps;
|
type IProps = NodeEditorProps;
|
||||||
|
|
||||||
const ImageEditor: FC<IProps> = () => {
|
const ImageEditor: FC<IProps> = () => {
|
||||||
const { pending, files, setFiles, uploadFiles } = useFileUploaderContext()!;
|
const { pending, files, setFiles, uploadFiles } = useUploaderContext()!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<ImageGrid files={files} setFiles={setFiles} locked={pending} />
|
<ImageGrid files={files} setFiles={setFiles} locked={values(pending)} />
|
||||||
</div>
|
</div>
|
||||||
</UploadDropzone>
|
</UploadDropzone>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,14 +2,14 @@ import React, { FC, useCallback } from 'react';
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { IFile } from '~/redux/types';
|
import { IFile } from '~/redux/types';
|
||||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
|
||||||
import { moveArrItem } from '~/utils/fn';
|
import { moveArrItem } from '~/utils/fn';
|
||||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
setFiles: (val: IFile[]) => void;
|
setFiles: (val: IFile[]) => void;
|
||||||
locked: IUploadStatus[];
|
locked: UploadStatus[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { AudioUpload } from '~/components/upload/AudioUpload';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { SortableAudioGridItem } from '~/components/editors/SortableAudioGridItem';
|
import { SortableAudioGridItem } from '~/components/editors/SortableAudioGridItem';
|
||||||
import { IFile } from '~/redux/types';
|
import { IFile } from '~/redux/types';
|
||||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
|
||||||
const SortableAudioGrid = SortableContainer(
|
const SortableAudioGrid = SortableContainer(
|
||||||
({
|
({
|
||||||
|
@ -15,7 +15,7 @@ const SortableAudioGrid = SortableContainer(
|
||||||
onTitleChange,
|
onTitleChange,
|
||||||
}: {
|
}: {
|
||||||
items: IFile[];
|
items: IFile[];
|
||||||
locked: IUploadStatus[];
|
locked: UploadStatus[];
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
onDelete: (file_id: IFile['id']) => void;
|
||||||
onTitleChange: (file_id: IFile['id'], title: string) => void;
|
onTitleChange: (file_id: IFile['id'], title: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -35,7 +35,7 @@ const SortableAudioGrid = SortableContainer(
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{locked.map((item, index) => (
|
{locked.map((item, index) => (
|
||||||
<SortableAudioGridItem key={item.temp_id} index={index} collection={1} disabled>
|
<SortableAudioGridItem key={item.id} index={index} collection={1} disabled>
|
||||||
<AudioUpload title={item.name} progress={item.progress} is_uploading />
|
<AudioUpload title={item.name} progress={item.progress} is_uploading />
|
||||||
</SortableAudioGridItem>
|
</SortableAudioGridItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { ImageUpload } from '~/components/upload/ImageUpload';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem';
|
import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem';
|
||||||
import { IFile } from '~/redux/types';
|
import { IFile } from '~/redux/types';
|
||||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
|
||||||
const SortableImageGrid = SortableContainer(
|
const SortableImageGrid = SortableContainer(
|
||||||
({
|
({
|
||||||
|
@ -17,7 +17,7 @@ const SortableImageGrid = SortableContainer(
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
items: IFile[];
|
items: IFile[];
|
||||||
locked: IUploadStatus[];
|
locked: UploadStatus[];
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
onDelete: (file_id: IFile['id']) => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -35,8 +35,8 @@ const SortableImageGrid = SortableContainer(
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{locked.map((item, index) => (
|
{locked.map((item, index) => (
|
||||||
<SortableImageGridItem key={item.temp_id} index={index} collection={1} disabled>
|
<SortableImageGridItem key={item.id} index={index} collection={1} disabled>
|
||||||
<ImageUpload thumb={item.preview} progress={item.progress} is_uploading />
|
<ImageUpload thumb={item.thumbnail} progress={item.progress} is_uploading />
|
||||||
</SortableImageGridItem>
|
</SortableImageGridItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
&:global(.is_uploading) {
|
&:global(.is_uploading) {
|
||||||
.thumb {
|
.thumb {
|
||||||
filter: blur(16px);
|
filter: blur(10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@
|
||||||
background: no-repeat 50% 50%;
|
background: no-repeat 50% 50%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
// filter: saturate(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
|
|
51
src/constants/uploads/index.ts
Normal file
51
src/constants/uploads/index.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
|
||||||
|
export const EMPTY_FILE: IFile = {
|
||||||
|
id: undefined,
|
||||||
|
user_id: undefined,
|
||||||
|
node_id: undefined,
|
||||||
|
|
||||||
|
name: '',
|
||||||
|
orig_name: '',
|
||||||
|
path: '',
|
||||||
|
full_path: '',
|
||||||
|
url: '',
|
||||||
|
size: 0,
|
||||||
|
type: undefined,
|
||||||
|
mime: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// for targeted cancellation
|
||||||
|
export enum UploadSubject {
|
||||||
|
Editor = 'editor',
|
||||||
|
Comment = 'comment',
|
||||||
|
Avatar = 'avatar',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UploadTarget {
|
||||||
|
Nodes = 'nodes',
|
||||||
|
Comments = 'comments',
|
||||||
|
Profiles = 'profiles',
|
||||||
|
Others = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UploadType {
|
||||||
|
Image = 'image',
|
||||||
|
Audio = 'audio',
|
||||||
|
Video = 'video',
|
||||||
|
Other = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FILE_MIMES: Record<UploadType, string[]> = {
|
||||||
|
[UploadType.Video]: [],
|
||||||
|
[UploadType.Image]: ['image/jpeg', 'image/jpg', 'image/png'],
|
||||||
|
[UploadType.Audio]: ['audio/mpeg3', 'audio/mpeg', 'audio/mp3'],
|
||||||
|
[UploadType.Other]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMMENT_FILE_TYPES = [
|
||||||
|
...FILE_MIMES[UploadType.Image],
|
||||||
|
...FILE_MIMES[UploadType.Audio],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
|
|
@ -6,8 +6,7 @@ import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import { useNodeFormFormik } from '~/hooks/node/useNodeFormFormik';
|
import { useNodeFormFormik } from '~/hooks/node/useNodeFormFormik';
|
||||||
import { EditorButtons } from '~/components/editors/EditorButtons';
|
import { EditorButtons } from '~/components/editors/EditorButtons';
|
||||||
import { FileUploaderProvider, useFileUploader } from '~/hooks/data/useFileUploader';
|
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
|
||||||
import { FormikProvider } from 'formik';
|
import { FormikProvider } from 'formik';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||||
|
@ -15,16 +14,19 @@ import { useTranslatedError } from '~/hooks/data/useTranslatedError';
|
||||||
import { useCloseOnEscape } from '~/hooks';
|
import { useCloseOnEscape } from '~/hooks';
|
||||||
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
|
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
|
||||||
import { IDialogProps } from '~/types/modal';
|
import { IDialogProps } from '~/types/modal';
|
||||||
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
|
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
interface Props extends IDialogProps {
|
interface Props extends IDialogProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
onSubmit: (node: INode) => Promise<unknown>;
|
onSubmit: (node: INode) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorDialog: FC<Props> = ({ node, onRequestClose, onSubmit }) => {
|
const EditorDialog: FC<Props> = observer(({ node, onRequestClose, onSubmit }) => {
|
||||||
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
||||||
|
|
||||||
const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, node.files);
|
const uploader = useUploader(UploadSubject.Editor, UploadTarget.Nodes, node.files);
|
||||||
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
|
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
|
||||||
const { values, handleSubmit, dirty, status } = formik;
|
const { values, handleSubmit, dirty, status } = formik;
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ const EditorDialog: FC<Props> = ({ node, onRequestClose, onSubmit }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalWrapper onOverlayClick={onClose}>
|
<ModalWrapper onOverlayClick={onClose}>
|
||||||
<FileUploaderProvider value={uploader}>
|
<UploaderContextProvider value={uploader}>
|
||||||
<FormikProvider value={formik}>
|
<FormikProvider value={formik}>
|
||||||
<form onSubmit={handleSubmit} className={styles.form}>
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
<BetterScrollDialog
|
<BetterScrollDialog
|
||||||
|
@ -78,9 +80,9 @@ const EditorDialog: FC<Props> = ({ node, onRequestClose, onSubmit }) => {
|
||||||
</BetterScrollDialog>
|
</BetterScrollDialog>
|
||||||
</form>
|
</form>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</FileUploaderProvider>
|
</UploaderContextProvider>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export { EditorDialog };
|
export { EditorDialog };
|
||||||
|
|
|
@ -1,104 +1,75 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { ChangeEvent, FC, useCallback } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { path, pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors';
|
import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||||
import { IFileWithUUID } from '~/redux/types';
|
|
||||||
import uuid from 'uuid4';
|
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: pick(['id'], selectAuthUser(state)),
|
user: pick(['id'], selectAuthUser(state)),
|
||||||
profile: pick(['is_loading', 'user'], selectAuthProfile(state)),
|
profile: pick(['is_loading', 'user'], selectAuthProfile(state)),
|
||||||
uploads: pick(['statuses', 'files'], selectUploads(state)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
|
||||||
authPatchUser: AUTH_ACTIONS.authPatchUser,
|
authPatchUser: AUTH_ACTIONS.authPatchUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
const ProfileAvatarUnconnected: FC<IProps> = ({
|
const ProfileAvatarUnconnected: FC<IProps> = observer(
|
||||||
user: { id },
|
({ user: { id }, profile: { is_loading, user }, authPatchUser }) => {
|
||||||
profile: { is_loading, user },
|
const uploader = useUploader(
|
||||||
uploads: { statuses, files },
|
UploadSubject.Avatar,
|
||||||
uploadUploadFiles,
|
UploadTarget.Profiles,
|
||||||
authPatchUser,
|
user?.photo ? [] : []
|
||||||
}) => {
|
);
|
||||||
const can_edit = !is_loading && id && id === user?.id;
|
|
||||||
|
|
||||||
const [temp, setTemp] = useState<string>('');
|
const onInputChange = useCallback(
|
||||||
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
try {
|
||||||
|
if (!event.target.files?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const photo = await uploader.uploadFile(event.target.files[0]);
|
||||||
if (!can_edit) return;
|
authPatchUser({ photo });
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[uploader, authPatchUser]
|
||||||
|
);
|
||||||
|
|
||||||
Object.entries(statuses).forEach(([id, status]) => {
|
const can_edit = !is_loading && id && id === user?.id;
|
||||||
if (temp === id && !!status.uuid && files[status.uuid]) {
|
|
||||||
authPatchUser({ photo: files[status.uuid] });
|
|
||||||
setTemp('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [statuses, files, temp, can_edit, authPatchUser]);
|
|
||||||
|
|
||||||
const onUpload = useCallback(
|
const backgroundImage = is_loading
|
||||||
(uploads: File[]) => {
|
? undefined
|
||||||
const items: IFileWithUUID[] = Array.from(uploads).map(
|
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`;
|
||||||
(file: File): IFileWithUUID => ({
|
|
||||||
file,
|
|
||||||
temp_id: uuid(),
|
|
||||||
subject: UPLOAD_SUBJECTS.AVATAR,
|
|
||||||
target: UPLOAD_TARGETS.PROFILES,
|
|
||||||
type: UPLOAD_TYPES.IMAGE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setTemp(path([0, 'temp_id'], items) || '');
|
return (
|
||||||
uploadUploadFiles(items.slice(0, 1));
|
<div
|
||||||
},
|
className={styles.avatar}
|
||||||
[uploadUploadFiles, setTemp]
|
style={{
|
||||||
);
|
backgroundImage,
|
||||||
|
}}
|
||||||
const onInputChange = useCallback(
|
>
|
||||||
event => {
|
{can_edit && <input type="file" onInput={onInputChange} />}
|
||||||
if (!can_edit) return;
|
{can_edit && (
|
||||||
|
<div className={styles.can_edit}>
|
||||||
event.preventDefault();
|
<Icon icon="photo_add" />
|
||||||
|
</div>
|
||||||
if (!event.target.files || !event.target.files.length) return;
|
)}
|
||||||
|
</div>
|
||||||
onUpload(Array.from(event.target.files));
|
);
|
||||||
},
|
}
|
||||||
[onUpload, can_edit]
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundImage = is_loading
|
|
||||||
? undefined
|
|
||||||
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.avatar}
|
|
||||||
style={{
|
|
||||||
backgroundImage,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{can_edit && <input type="file" onInput={onInputChange} />}
|
|
||||||
{can_edit && (
|
|
||||||
<div className={styles.can_edit}>
|
|
||||||
<Icon icon="photo_add" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProfileAvatar = connect(mapStateToProps, mapDispatchToProps)(ProfileAvatarUnconnected);
|
const ProfileAvatar = connect(mapStateToProps, mapDispatchToProps)(ProfileAvatarUnconnected);
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { IComment, INode } from '~/redux/types';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||||
import { array, object, string } from 'yup';
|
import { array, object, string } from 'yup';
|
||||||
import { FileUploader } from '~/hooks/data/useFileUploader';
|
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
import { hasPath, path } from 'ramda';
|
import { hasPath, path } from 'ramda';
|
||||||
|
import { Uploader } from '~/utils/context/UploaderContextProvider';
|
||||||
|
|
||||||
const validationSchema = object().shape({
|
const validationSchema = object().shape({
|
||||||
text: string(),
|
text: string(),
|
||||||
|
@ -31,7 +31,7 @@ const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComme
|
||||||
export const useCommentFormFormik = (
|
export const useCommentFormFormik = (
|
||||||
values: IComment,
|
values: IComment,
|
||||||
nodeId: INode['id'],
|
nodeId: INode['id'],
|
||||||
uploader: FileUploader,
|
uploader: Uploader,
|
||||||
sendData: (data: IComment) => Promise<unknown>,
|
sendData: (data: IComment) => Promise<unknown>,
|
||||||
stopEditing?: () => void
|
stopEditing?: () => void
|
||||||
) => {
|
) => {
|
||||||
|
@ -41,20 +41,19 @@ export const useCommentFormFormik = (
|
||||||
async (values: IComment, helpers: FormikHelpers<IComment>) => {
|
async (values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||||
try {
|
try {
|
||||||
helpers.setSubmitting(true);
|
helpers.setSubmitting(true);
|
||||||
await sendData(values);
|
await sendData({ ...values, files: uploader.files });
|
||||||
onSuccess(helpers)();
|
onSuccess(helpers)();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onSuccess(helpers)(error);
|
onSuccess(helpers)(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sendData]
|
[sendData, uploader.files]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
uploader.setFiles([]);
|
uploader.setFiles([]);
|
||||||
|
|
||||||
if (stopEditing) stopEditing();
|
if (stopEditing) stopEditing();
|
||||||
}, [uploader, stopEditing]);
|
}, [stopEditing, uploader]);
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
FC,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { IFile, IFileWithUUID } from '~/redux/types';
|
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
|
||||||
import { getFileType } from '~/utils/uploader';
|
|
||||||
import uuid from 'uuid4';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
|
||||||
import { path } from 'ramda';
|
|
||||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
|
||||||
|
|
||||||
export const useFileUploader = (
|
|
||||||
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
|
||||||
target: typeof UPLOAD_TARGETS[keyof typeof UPLOAD_TARGETS],
|
|
||||||
initialFiles?: IFile[]
|
|
||||||
) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { files: uploadedFiles, statuses } = useShallowSelect(selectUploads);
|
|
||||||
|
|
||||||
const [files, setFiles] = useState<IFile[]>(initialFiles || []);
|
|
||||||
const [pendingIDs, setPendingIDs] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const uploadFiles = useCallback(
|
|
||||||
(files: File[]) => {
|
|
||||||
const items: IFileWithUUID[] = files.map(
|
|
||||||
(file: File): IFileWithUUID => ({
|
|
||||||
file,
|
|
||||||
temp_id: uuid(),
|
|
||||||
subject,
|
|
||||||
target,
|
|
||||||
type: getFileType(file),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const temps = items.filter(el => !!el.temp_id).map(file => file.temp_id!);
|
|
||||||
|
|
||||||
setPendingIDs([...pendingIDs, ...temps]);
|
|
||||||
dispatch(uploadUploadFiles(items));
|
|
||||||
},
|
|
||||||
[pendingIDs, setPendingIDs, dispatch, subject, target]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const added = pendingIDs
|
|
||||||
.map(temp_uuid => path([temp_uuid, 'uuid'], statuses) as IUploadStatus['uuid'])
|
|
||||||
.filter(el => el)
|
|
||||||
.map(el => (path([String(el)], uploadedFiles) as IFile) || undefined)
|
|
||||||
.filter(el => !!el! && !files.some(file => file && file.id === el.id));
|
|
||||||
|
|
||||||
const newPending = pendingIDs.filter(
|
|
||||||
temp_id =>
|
|
||||||
statuses[temp_id] &&
|
|
||||||
(!statuses[temp_id].uuid || !added.some(file => file.id === statuses[temp_id].uuid))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (added.length) {
|
|
||||||
setPendingIDs(newPending);
|
|
||||||
setFiles([...files, ...added]);
|
|
||||||
}
|
|
||||||
}, [statuses, files, pendingIDs, setFiles, setPendingIDs, uploadedFiles]);
|
|
||||||
|
|
||||||
const pending = useMemo(() => pendingIDs.map(id => statuses[id]).filter(el => !!el), [
|
|
||||||
statuses,
|
|
||||||
pendingIDs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isLoading = pending.length > 0;
|
|
||||||
|
|
||||||
return { uploadFiles, pending, files, setFiles, isUploading: isLoading };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FileUploader = ReturnType<typeof useFileUploader>;
|
|
||||||
const FileUploaderContext = createContext<FileUploader | undefined>(undefined);
|
|
||||||
|
|
||||||
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
|
||||||
value,
|
|
||||||
children,
|
|
||||||
}) => <FileUploaderContext.Provider value={value}>{children}</FileUploaderContext.Provider>;
|
|
||||||
|
|
||||||
export const useFileUploaderContext = () => useContext(FileUploaderContext);
|
|
63
src/hooks/data/useUploader.ts
Normal file
63
src/hooks/data/useUploader.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { apiUploadFile } from '~/api/uploads';
|
||||||
|
import { keys } from 'ramda';
|
||||||
|
import { useLocalObservable } from 'mobx-react-lite';
|
||||||
|
import { UploaderStore } from '~/store/uploader/UploaderStore';
|
||||||
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
import uuid from 'uuid4';
|
||||||
|
|
||||||
|
export const useUploader = (
|
||||||
|
subject: UploadSubject,
|
||||||
|
target: UploadTarget,
|
||||||
|
initialFiles?: IFile[]
|
||||||
|
) => {
|
||||||
|
const store = useLocalObservable(() => new UploaderStore(initialFiles));
|
||||||
|
|
||||||
|
const uploadFile = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: pass CancelationToken for axios as cancel() to pending
|
||||||
|
// TODO: cancel all uploads on unmount
|
||||||
|
|
||||||
|
const pending = await store.addPending(id, file);
|
||||||
|
const onProgress = ({ loaded, total }) => store.updateProgress(id, loaded, total);
|
||||||
|
const result = await apiUploadFile({ file, target, type: pending.type, onProgress });
|
||||||
|
|
||||||
|
store.removePending(id);
|
||||||
|
store.addFile(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
store.removePending(id);
|
||||||
|
showErrorToast(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[store, target]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadFiles = useCallback(
|
||||||
|
async (files: File[]) => {
|
||||||
|
await Promise.any(files.map(file => uploadFile(file)));
|
||||||
|
},
|
||||||
|
[uploadFile]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isUploading = keys(store.pending).length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadFile,
|
||||||
|
uploadFiles,
|
||||||
|
files: store.files,
|
||||||
|
filesImages: store.filesImages,
|
||||||
|
filesAudios: store.filesAudios,
|
||||||
|
pending: store.pending,
|
||||||
|
pendingImages: store.pendingImages,
|
||||||
|
pendingAudios: store.pendingAudios,
|
||||||
|
isUploading,
|
||||||
|
setFiles: store.setFiles,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,13 +1,13 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
|
|
||||||
export const useNodeAudios = (node: INode) => {
|
export const useNodeAudios = (node: INode) => {
|
||||||
if (!node?.files) {
|
if (!node?.files) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Audio), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { FileUploader } from '~/hooks/data/useFileUploader';
|
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
|
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
import { keys } from 'ramda';
|
import { keys } from 'ramda';
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
import { Uploader } from '~/utils/context/UploaderContextProvider';
|
||||||
|
|
||||||
const validationSchema = object().shape({});
|
const validationSchema = object().shape({});
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INod
|
||||||
|
|
||||||
export const useNodeFormFormik = (
|
export const useNodeFormFormik = (
|
||||||
values: INode,
|
values: INode,
|
||||||
uploader: FileUploader,
|
uploader: Uploader,
|
||||||
stopEditing: () => void,
|
stopEditing: () => void,
|
||||||
sendSaveRequest: (node: INode) => Promise<unknown>
|
sendSaveRequest: (node: INode) => Promise<unknown>
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UploadType } from '~/constants/uploads';
|
||||||
|
|
||||||
export const useNodeImages = (node: INode) => {
|
export const useNodeImages = (node: INode) => {
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Image), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,9 +11,6 @@ import auth from '~/redux/auth';
|
||||||
import authSaga from '~/redux/auth/sagas';
|
import authSaga from '~/redux/auth/sagas';
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
|
||||||
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
|
||||||
import uploadSaga from '~/redux/uploads/sagas';
|
|
||||||
|
|
||||||
import player, { IPlayerState } from '~/redux/player/reducer';
|
import player, { IPlayerState } from '~/redux/player/reducer';
|
||||||
import playerSaga from '~/redux/player/sagas';
|
import playerSaga from '~/redux/player/sagas';
|
||||||
|
|
||||||
|
@ -41,7 +38,6 @@ const playerPersistConfig: PersistConfig = {
|
||||||
export interface IState {
|
export interface IState {
|
||||||
auth: IAuthState;
|
auth: IAuthState;
|
||||||
router: RouterState;
|
router: RouterState;
|
||||||
uploads: IUploadState;
|
|
||||||
player: IPlayerState;
|
player: IPlayerState;
|
||||||
messages: IMessagesState;
|
messages: IMessagesState;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +56,6 @@ export const store = createStore(
|
||||||
combineReducers<IState>({
|
combineReducers<IState>({
|
||||||
auth: persistReducer(authPersistConfig, auth),
|
auth: persistReducer(authPersistConfig, auth),
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
uploads,
|
|
||||||
player: persistReducer(playerPersistConfig, player),
|
player: persistReducer(playerPersistConfig, player),
|
||||||
messages,
|
messages,
|
||||||
}),
|
}),
|
||||||
|
@ -72,7 +67,6 @@ export function configureStore(): {
|
||||||
persistor: Persistor;
|
persistor: Persistor;
|
||||||
} {
|
} {
|
||||||
sagaMiddleware.run(authSaga);
|
sagaMiddleware.run(authSaga);
|
||||||
sagaMiddleware.run(uploadSaga);
|
|
||||||
sagaMiddleware.run(playerSaga);
|
sagaMiddleware.run(playerSaga);
|
||||||
sagaMiddleware.run(messagesSaga);
|
sagaMiddleware.run(messagesSaga);
|
||||||
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { UPLOAD_ACTIONS } from '~/redux/uploads/constants';
|
|
||||||
import { IFile, IFileWithUUID, UUID } from '../types';
|
|
||||||
import { IUploadStatus } from './reducer';
|
|
||||||
|
|
||||||
export const uploadUploadFiles = (files: IFileWithUUID[]) => ({
|
|
||||||
files,
|
|
||||||
type: UPLOAD_ACTIONS.UPLOAD_FILES,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const uploadAddStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
|
||||||
temp_id,
|
|
||||||
status,
|
|
||||||
type: UPLOAD_ACTIONS.ADD_STATUS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const uploadAddFile = (file: IFile) => ({
|
|
||||||
file,
|
|
||||||
type: UPLOAD_ACTIONS.ADD_FILE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const uploadSetStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
|
||||||
temp_id,
|
|
||||||
status,
|
|
||||||
type: UPLOAD_ACTIONS.SET_STATUS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const uploadDropStatus = (temp_id: UUID) => ({
|
|
||||||
temp_id,
|
|
||||||
type: UPLOAD_ACTIONS.DROP_STATUS,
|
|
||||||
});
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { IFile, IUploadType } from '~/redux/types';
|
|
||||||
import { IUploadStatus } from './reducer';
|
|
||||||
|
|
||||||
const prefix = 'UPLOAD.';
|
|
||||||
|
|
||||||
export const UPLOAD_ACTIONS = {
|
|
||||||
UPLOAD_FILES: `${prefix}UPLOAD_FILES`,
|
|
||||||
UPLOAD_CANCEL: `${prefix}UPLOAD_CANCEL`,
|
|
||||||
|
|
||||||
ADD_STATUS: `${prefix}ADD_STATUS`,
|
|
||||||
DROP_STATUS: `${prefix}DROP_STATUS`,
|
|
||||||
SET_STATUS: `${prefix}SET_STATUS`,
|
|
||||||
|
|
||||||
ADD_FILE: `${prefix}ADD_FILE`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EMPTY_FILE: IFile = {
|
|
||||||
id: undefined,
|
|
||||||
user_id: undefined,
|
|
||||||
node_id: undefined,
|
|
||||||
|
|
||||||
name: '',
|
|
||||||
orig_name: '',
|
|
||||||
path: '',
|
|
||||||
full_path: '',
|
|
||||||
url: '',
|
|
||||||
size: 0,
|
|
||||||
type: undefined,
|
|
||||||
mime: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
|
||||||
is_uploading: false,
|
|
||||||
preview: '',
|
|
||||||
error: '',
|
|
||||||
uuid: 0,
|
|
||||||
url: '',
|
|
||||||
progress: 0,
|
|
||||||
thumbnail_url: '',
|
|
||||||
type: '',
|
|
||||||
temp_id: '',
|
|
||||||
name: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// for targeted cancellation
|
|
||||||
export const UPLOAD_SUBJECTS = {
|
|
||||||
EDITOR: 'editor',
|
|
||||||
COMMENT: 'comment',
|
|
||||||
AVATAR: 'avatar',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UPLOAD_TARGETS = {
|
|
||||||
NODES: 'nodes',
|
|
||||||
COMMENTS: 'comments',
|
|
||||||
PROFILES: 'profiles',
|
|
||||||
OTHER: 'other',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UPLOAD_TYPES: Record<string, IUploadType> = {
|
|
||||||
IMAGE: 'image',
|
|
||||||
AUDIO: 'audio',
|
|
||||||
VIDEO: 'video',
|
|
||||||
OTHER: 'other',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FILE_MIMES = {
|
|
||||||
[UPLOAD_TYPES.VIDEO]: [],
|
|
||||||
[UPLOAD_TYPES.IMAGE]: ['image/jpeg', 'image/jpg', 'image/png'],
|
|
||||||
[UPLOAD_TYPES.AUDIO]: ['audio/mpeg3', 'audio/mpeg', 'audio/mp3'],
|
|
||||||
[UPLOAD_TYPES.OTHER]: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const COMMENT_FILE_TYPES = [
|
|
||||||
...FILE_MIMES[UPLOAD_TYPES.IMAGE],
|
|
||||||
...FILE_MIMES[UPLOAD_TYPES.AUDIO],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const IMAGE_FILE_TYPES = [...FILE_MIMES[UPLOAD_TYPES.IMAGE]];
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { assocPath, omit } from 'ramda';
|
|
||||||
|
|
||||||
import { EMPTY_UPLOAD_STATUS, UPLOAD_ACTIONS } from './constants';
|
|
||||||
import { uploadAddFile, uploadAddStatus, uploadDropStatus, uploadSetStatus } from './actions';
|
|
||||||
import { IUploadState } from './reducer';
|
|
||||||
|
|
||||||
const addStatus = (
|
|
||||||
state: IUploadState,
|
|
||||||
{ temp_id, status }: ReturnType<typeof uploadAddStatus>
|
|
||||||
): IUploadState =>
|
|
||||||
assocPath(
|
|
||||||
['statuses'],
|
|
||||||
{ ...state.statuses, [temp_id]: { ...EMPTY_UPLOAD_STATUS, ...status } },
|
|
||||||
state
|
|
||||||
);
|
|
||||||
|
|
||||||
const dropStatus = (
|
|
||||||
state: IUploadState,
|
|
||||||
{ temp_id }: ReturnType<typeof uploadDropStatus>
|
|
||||||
): IUploadState => assocPath(['statuses'], omit([temp_id], state.statuses), state);
|
|
||||||
|
|
||||||
const setStatus = (
|
|
||||||
state: IUploadState,
|
|
||||||
{ temp_id, status }: ReturnType<typeof uploadSetStatus>
|
|
||||||
): IUploadState =>
|
|
||||||
assocPath(
|
|
||||||
['statuses'],
|
|
||||||
{
|
|
||||||
...state.statuses,
|
|
||||||
[temp_id]: { ...(state.statuses[temp_id] || EMPTY_UPLOAD_STATUS), ...status },
|
|
||||||
},
|
|
||||||
state
|
|
||||||
);
|
|
||||||
|
|
||||||
const addFile = (state: IUploadState, { file }: ReturnType<typeof uploadAddFile>): IUploadState => {
|
|
||||||
if (!file.id) return state;
|
|
||||||
return assocPath(['files', file.id], file, state);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UPLOAD_HANDLERS = {
|
|
||||||
[UPLOAD_ACTIONS.ADD_STATUS]: addStatus,
|
|
||||||
[UPLOAD_ACTIONS.DROP_STATUS]: dropStatus,
|
|
||||||
[UPLOAD_ACTIONS.SET_STATUS]: setStatus,
|
|
||||||
[UPLOAD_ACTIONS.ADD_FILE]: addFile,
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
|
||||||
import { IFile, UUID } from '~/redux/types';
|
|
||||||
import { UPLOAD_HANDLERS } from './handlers';
|
|
||||||
|
|
||||||
export interface IUploadStatus {
|
|
||||||
is_uploading: boolean;
|
|
||||||
error: string;
|
|
||||||
preview: string;
|
|
||||||
uuid: IFile['id'];
|
|
||||||
url: string;
|
|
||||||
type: string;
|
|
||||||
thumbnail_url: string;
|
|
||||||
progress: number;
|
|
||||||
temp_id: UUID;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUploadState {
|
|
||||||
files: Record<UUID, IFile>;
|
|
||||||
statuses: Record<UUID, IUploadStatus>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
|
||||||
files: {},
|
|
||||||
statuses: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createReducer(INITIAL_STATE, UPLOAD_HANDLERS);
|
|
|
@ -1,149 +0,0 @@
|
||||||
import { SagaIterator } from 'redux-saga';
|
|
||||||
import { all, call, fork, put, race, spawn, take, takeEvery } from 'redux-saga/effects';
|
|
||||||
import { apiUploadFile } from './api';
|
|
||||||
import { FILE_MIMES, UPLOAD_ACTIONS } from '~/redux/uploads/constants';
|
|
||||||
import {
|
|
||||||
uploadAddFile,
|
|
||||||
uploadAddStatus,
|
|
||||||
uploadDropStatus,
|
|
||||||
uploadSetStatus,
|
|
||||||
uploadUploadFiles,
|
|
||||||
} from './actions';
|
|
||||||
import { createUploader, uploadGetThumb } from '~/utils/uploader';
|
|
||||||
import { HTTP_RESPONSES } from '~/utils/api';
|
|
||||||
import { IFileWithUUID, IUploadProgressHandler, Unwrap } from '../types';
|
|
||||||
|
|
||||||
function* uploadCall({
|
|
||||||
file,
|
|
||||||
temp_id,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
onProgress,
|
|
||||||
}: IFileWithUUID & { onProgress: IUploadProgressHandler }) {
|
|
||||||
const data: Unwrap<typeof apiUploadFile> = yield call(apiUploadFile, {
|
|
||||||
file,
|
|
||||||
temp_id,
|
|
||||||
type,
|
|
||||||
target,
|
|
||||||
onProgress,
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onUploadProgress(chan) {
|
|
||||||
while (true) {
|
|
||||||
const { progress, temp_id }: { progress: number; temp_id: string } = yield take(chan);
|
|
||||||
|
|
||||||
yield put(uploadSetStatus(temp_id, { progress }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* uploadCancelWorker(id) {
|
|
||||||
while (true) {
|
|
||||||
const { temp_id } = yield take(UPLOAD_ACTIONS.UPLOAD_CANCEL);
|
|
||||||
if (temp_id === id) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* uploadWorker({
|
|
||||||
file,
|
|
||||||
temp_id,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
}: IFileWithUUID): SagaIterator<Unwrap<typeof uploadCall>> {
|
|
||||||
const [promise, chan] = createUploader<Partial<IFileWithUUID>, Partial<IFileWithUUID>>(
|
|
||||||
uploadCall,
|
|
||||||
{ temp_id, target, type }
|
|
||||||
);
|
|
||||||
|
|
||||||
yield fork(onUploadProgress, chan);
|
|
||||||
|
|
||||||
return yield call(promise, {
|
|
||||||
temp_id,
|
|
||||||
file,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function* uploadFile({ file, temp_id, type, target, onSuccess, onFail }: IFileWithUUID) {
|
|
||||||
if (!temp_id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!file.type || !type || !FILE_MIMES[type] || !FILE_MIMES[type].includes(file.type)) {
|
|
||||||
return {
|
|
||||||
error: 'File_Not_Image',
|
|
||||||
status: HTTP_RESPONSES.BAD_REQUEST,
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const preview: Unwrap<typeof uploadGetThumb> = yield call(uploadGetThumb, file);
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
uploadAddStatus(temp_id, {
|
|
||||||
preview: preview.toString(),
|
|
||||||
is_uploading: true,
|
|
||||||
temp_id,
|
|
||||||
type,
|
|
||||||
name: file.name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const [result, cancel]: [
|
|
||||||
Unwrap<typeof uploadCall>,
|
|
||||||
Unwrap<typeof uploadCancelWorker>
|
|
||||||
] = yield race([
|
|
||||||
call(uploadWorker, {
|
|
||||||
file,
|
|
||||||
temp_id,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
}),
|
|
||||||
call(uploadCancelWorker, temp_id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (cancel || !result) {
|
|
||||||
if (onFail) onFail();
|
|
||||||
return yield put(uploadDropStatus(temp_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
uploadSetStatus(temp_id, {
|
|
||||||
is_uploading: false,
|
|
||||||
error: '',
|
|
||||||
uuid: result.id,
|
|
||||||
url: result.full_path,
|
|
||||||
type,
|
|
||||||
thumbnail_url: result.full_path,
|
|
||||||
progress: 1,
|
|
||||||
name: file.name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield put(uploadAddFile(result));
|
|
||||||
|
|
||||||
if (onSuccess) onSuccess(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (onFail) onFail();
|
|
||||||
|
|
||||||
return yield put(
|
|
||||||
uploadSetStatus(temp_id, {
|
|
||||||
is_uploading: false,
|
|
||||||
error: error.message,
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* uploadFiles({ files }: ReturnType<typeof uploadUploadFiles>) {
|
|
||||||
yield all(files.map(file => spawn(uploadFile, file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function*() {
|
|
||||||
yield takeEvery(UPLOAD_ACTIONS.UPLOAD_FILES, uploadFiles);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { IState } from '~/redux/store';
|
|
||||||
import { IUploadState } from '~/redux/uploads/reducer';
|
|
||||||
|
|
||||||
export const selectUploads = ({ uploads }: IState): IUploadState => uploads;
|
|
||||||
export const selectUploadStatuses = ({ uploads: { statuses } }: IState): IUploadState['statuses'] => statuses;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { IFile, IFileWithUUID, IUploadProgressHandler } from '~/redux/types';
|
|
||||||
|
|
||||||
export type ApiUploadFileRequest = IFileWithUUID & {
|
|
||||||
onProgress: IUploadProgressHandler;
|
|
||||||
};
|
|
||||||
export type ApiUploadFIleResult = IFile;
|
|
79
src/store/uploader/UploaderStore.ts
Normal file
79
src/store/uploader/UploaderStore.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { makeAutoObservable, runInAction } from 'mobx';
|
||||||
|
import { IFile, UUID } from '~/redux/types';
|
||||||
|
import { getFileType, uploadGetThumb } from '~/utils/uploader';
|
||||||
|
import { has, omit, values } from 'ramda';
|
||||||
|
import { UploadType } from '~/constants/uploads';
|
||||||
|
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
|
||||||
|
|
||||||
|
export interface UploadStatus {
|
||||||
|
id: UUID;
|
||||||
|
thumbnail: string;
|
||||||
|
size: number;
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
type: UploadType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploaderStore {
|
||||||
|
pending: Record<UUID, UploadStatus> = {};
|
||||||
|
|
||||||
|
constructor(public files: IFile[] = []) {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile = (file: IFile) => this.files.push(file);
|
||||||
|
setFiles = (files: IFile[]) => (this.files = files);
|
||||||
|
|
||||||
|
/** adds pending from file */
|
||||||
|
addPending = async (id: string, file: File) => {
|
||||||
|
const thumbnail = await uploadGetThumb(file);
|
||||||
|
const size = file.size;
|
||||||
|
const name = file.name;
|
||||||
|
const progress = 0;
|
||||||
|
const type = getFileType(file);
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
throw new Error(ERROR_LITERAL[ERRORS.UNKNOWN_FILE_TYPE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.pending[id] = { id, thumbnail, size, name, progress, type };
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.pending[id];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** updates progress for file */
|
||||||
|
updateProgress = (id: UUID, loaded: number, total: number) => {
|
||||||
|
if (!has(id, this.pending)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending[id].progress = loaded / total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** removes pending item by id */
|
||||||
|
removePending = (id: UUID) => {
|
||||||
|
this.pending = omit([id], this.pending);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** returns only image files */
|
||||||
|
get filesImages() {
|
||||||
|
return this.files.filter(file => file && file.type === UploadType.Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns only image pending */
|
||||||
|
get pendingImages() {
|
||||||
|
return values(this.pending).filter(item => item.type === UploadType.Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns only audio files */
|
||||||
|
get filesAudios() {
|
||||||
|
return this.files.filter(file => file && file.type === UploadType.Audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns only audio pending */
|
||||||
|
get pendingAudios() {
|
||||||
|
return values(this.pending).filter(item => item.type === UploadType.Audio);
|
||||||
|
}
|
||||||
|
}
|
28
src/utils/context/UploaderContextProvider.tsx
Normal file
28
src/utils/context/UploaderContextProvider.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { createContext, FC, useContext } from 'react';
|
||||||
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { EMPTY_FILE } from '~/constants/uploads';
|
||||||
|
|
||||||
|
export type Uploader = ReturnType<typeof useUploader>;
|
||||||
|
|
||||||
|
const UploaderContext = createContext<Uploader>({
|
||||||
|
files: [],
|
||||||
|
filesAudios: [],
|
||||||
|
filesImages: [],
|
||||||
|
uploadFile: async () => EMPTY_FILE,
|
||||||
|
uploadFiles: async () => {},
|
||||||
|
pending: {},
|
||||||
|
pendingAudios: [],
|
||||||
|
pendingImages: [],
|
||||||
|
isUploading: false,
|
||||||
|
setFiles: (files: IFile[]) => files,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UploaderContextProvider: FC<{
|
||||||
|
value: Uploader;
|
||||||
|
children;
|
||||||
|
}> = ({ value, children }) => (
|
||||||
|
<UploaderContext.Provider value={value}>{children}</UploaderContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useUploaderContext = () => useContext(UploaderContext);
|
|
@ -1,81 +1,24 @@
|
||||||
import uuid from 'uuid4';
|
|
||||||
import { END, eventChannel, EventChannel } from 'redux-saga';
|
|
||||||
import { VALIDATORS } from '~/utils/validators';
|
import { VALIDATORS } from '~/utils/validators';
|
||||||
import { IFile, IResultWithStatus } from '~/redux/types';
|
import { FILE_MIMES, UploadType } from '~/constants/uploads';
|
||||||
import { HTTP_RESPONSES } from './api';
|
|
||||||
import { EMPTY_FILE, FILE_MIMES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
|
||||||
|
|
||||||
export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
|
|
||||||
|
|
||||||
export function createUploader<T extends {}, R extends {}>(
|
|
||||||
callback: (args: any) => any,
|
|
||||||
payload: R
|
|
||||||
): [
|
|
||||||
(args: T) => (args: T & { onProgress: (current: number, total: number) => void }) => any,
|
|
||||||
EventChannel<any>
|
|
||||||
] {
|
|
||||||
let emit;
|
|
||||||
|
|
||||||
const chan = eventChannel(emitter => {
|
|
||||||
emit = emitter;
|
|
||||||
return () => null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onProgress = ({ loaded, total }: { loaded: number; total: number }): void => {
|
|
||||||
emit(loaded >= total ? END : { ...payload, progress: parseFloat((loaded / total).toFixed(1)) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrappedCallback = args => callback({ ...args, onProgress });
|
|
||||||
|
|
||||||
return [wrappedCallback, chan];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** if file is image, returns data-uri of thumbnail */
|
||||||
export const uploadGetThumb = async file => {
|
export const uploadGetThumb = async file => {
|
||||||
if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) return '';
|
if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) return '';
|
||||||
|
|
||||||
return new Promise<string | ArrayBuffer>(resolve => {
|
return new Promise<string>(resolve => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => resolve(reader.result || '');
|
reader.onloadend = () => resolve(reader.result?.toString() || '');
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fakeUploader = ({
|
/** returns UploadType by file */
|
||||||
file,
|
export const getFileType = (file: File): UploadType | undefined =>
|
||||||
onProgress,
|
((file.type &&
|
||||||
mustSucceed,
|
Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) as UploadType) ||
|
||||||
}: {
|
|
||||||
file: { url?: string; error?: string };
|
|
||||||
onProgress: (current: number, total: number) => void;
|
|
||||||
mustSucceed: boolean;
|
|
||||||
}): Promise<IResultWithStatus<IFile>> => {
|
|
||||||
const { error } = file;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
onProgress(1, 3);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
onProgress(2, 3);
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
onProgress(3, 3);
|
|
||||||
if (mustSucceed) {
|
|
||||||
resolve({ status: HTTP_RESPONSES.CREATED, data: { ...EMPTY_FILE, id: uuid() } });
|
|
||||||
} else {
|
|
||||||
reject({ response: { statusText: error } });
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
|
|
||||||
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
|
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
// getImageFromPaste returns any images from paste event
|
/** getImageFromPaste returns any images from paste event */
|
||||||
export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => {
|
export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => {
|
||||||
const items = event.clipboardData?.items;
|
const items = event.clipboardData?.items;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import isValid from 'date-fns/isValid';
|
import isValid from 'date-fns/isValid';
|
||||||
import { IMAGE_MIME_TYPES } from '~/utils/uploader';
|
import { IMAGE_MIME_TYPES } from '~/constants/uploads';
|
||||||
|
|
||||||
const isValidEmail = (email: string): boolean =>
|
const isValidEmail = (email: string): boolean =>
|
||||||
!!email &&
|
!!email &&
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue