mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
refactored component errors
This commit is contained in:
parent
7031084b09
commit
d4c2e7ee09
79 changed files with 573 additions and 462 deletions
|
@ -33,7 +33,8 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalSho
|
||||||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
||||||
() =>
|
() =>
|
||||||
reduce(
|
reduce(
|
||||||
(group, file) => assocPath([file.type], append(file, group[file.type]), group),
|
(group, file) =>
|
||||||
|
file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
|
||||||
{},
|
{},
|
||||||
comment.files
|
comment.files
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { selectPlayer } from '~/redux/player/selectors';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as PLAYER_ACTIONS from '~/redux/player/actions';
|
import * as PLAYER_ACTIONS from '~/redux/player/actions';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import { path } from 'ramda';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
youtubes: selectPlayer(state).youtubes,
|
youtubes: selectPlayer(state).youtubes,
|
||||||
|
@ -21,30 +22,32 @@ type Props = ReturnType<typeof mapStateToProps> &
|
||||||
|
|
||||||
const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
({ block, youtubes, playerGetYoutubeInfo }) => {
|
({ block, youtubes, playerGetYoutubeInfo }) => {
|
||||||
const link = useMemo(
|
const id = useMemo(() => {
|
||||||
() =>
|
const match = block.content.match(
|
||||||
block.content.match(
|
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-\=]+)/
|
||||||
/https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?([\w\-\=]+)/
|
|
||||||
),
|
|
||||||
[block.content]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (match && match[1]) || '';
|
||||||
|
}, [block.content]);
|
||||||
|
|
||||||
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!link[5] || youtubes[link[5]]) return;
|
if (!id) return;
|
||||||
playerGetYoutubeInfo(link[5]);
|
playerGetYoutubeInfo(id);
|
||||||
}, [link, playerGetYoutubeInfo]);
|
}, [id, playerGetYoutubeInfo]);
|
||||||
|
|
||||||
const title = useMemo(
|
const title = useMemo<string>(() => {
|
||||||
() =>
|
if (!id) {
|
||||||
(youtubes[link[5]] && youtubes[link[5]].metadata && youtubes[link[5]].metadata.title) || '',
|
return block.content;
|
||||||
[link, youtubes]
|
}
|
||||||
);
|
|
||||||
|
return path([id, 'metadata', 'title'], youtubes) || block.content;
|
||||||
|
}, [id, youtubes, block.content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.embed}>
|
<div className={styles.embed}>
|
||||||
<a href={link[0]} target="_blank" />
|
<a href={id[0]} target="_blank" />
|
||||||
|
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||||
|
@ -53,7 +56,7 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
<Icon icon="play" size={32} />
|
<Icon icon="play" size={32} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.title}>{title || link[0]}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,7 +67,13 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
|
|
||||||
<Group horizontal className={styles.buttons}>
|
<Group horizontal className={styles.buttons}>
|
||||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||||
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
|
|
||||||
|
{!!textarea && (
|
||||||
|
<CommentFormFormatButtons
|
||||||
|
element={textarea}
|
||||||
|
handler={formik.handleChange('text')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading && <LoaderCircle size={20} />}
|
{isLoading && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
||||||
|
|
||||||
const CommentFormAttaches: FC = () => {
|
const CommentFormAttaches: FC = () => {
|
||||||
const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
|
const uploader = useFileUploaderContext();
|
||||||
|
const { files, pending, setFiles, uploadFiles } = uploader!;
|
||||||
|
|
||||||
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||||
files,
|
files,
|
||||||
|
@ -70,7 +71,7 @@ const CommentFormAttaches: FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAudioTitleChange = useCallback(
|
const onAudioTitleChange = useCallback(
|
||||||
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
(fileId: IFile['id'], title: string) => {
|
||||||
setFiles(
|
setFiles(
|
||||||
files.map(file =>
|
files.map(file =>
|
||||||
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||||
|
@ -80,8 +81,9 @@ const CommentFormAttaches: FC = () => {
|
||||||
[files, setFiles]
|
[files, setFiles]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!hasAttaches) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasAttaches && (
|
|
||||||
<div className={styles.attaches} onDropCapture={onDrop}>
|
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||||
{hasImageAttaches && (
|
{hasImageAttaches && (
|
||||||
<SortableImageGrid
|
<SortableImageGrid
|
||||||
|
@ -109,7 +111,6 @@ const CommentFormAttaches: FC = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ const LocalCommentFormTextarea: FC<IProps> = ({ setRef }) => {
|
||||||
|
|
||||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||||
({ ctrlKey, key }) => {
|
({ ctrlKey, key }) => {
|
||||||
if (!!ctrlKey && key === 'Enter') handleSubmit(null);
|
if (ctrlKey && key === 'Enter') handleSubmit(undefined);
|
||||||
},
|
},
|
||||||
[handleSubmit]
|
[handleSubmit]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { FC, useState, useCallback, useEffect, useRef } from "react";
|
import React, { FC, useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { IUser } from "~/redux/auth/types";
|
import { IUser } from '~/redux/auth/types';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
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';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
cover: IUser["cover"];
|
cover: IUser['cover'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
||||||
const ref = useRef<HTMLImageElement>();
|
const ref = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
const [is_loaded, setIsLoaded] = useState(false);
|
const [is_loaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cover || !cover.url || !ref || !ref.current) return;
|
if (!cover || !cover.url || !ref || !ref.current) return;
|
||||||
|
|
||||||
ref.current.src = "";
|
ref.current.src = '';
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
ref.current.src = getURL(cover, PRESETS.cover);
|
ref.current.src = getURL(cover, PRESETS.cover);
|
||||||
}, [cover]);
|
}, [cover]);
|
||||||
|
|
|
@ -16,7 +16,7 @@ const FullWidth: FC<IProps> = ({ children, onRefresh }) => {
|
||||||
const { width } = sample.current.getBoundingClientRect();
|
const { width } = sample.current.getBoundingClientRect();
|
||||||
const { clientWidth } = document.documentElement;
|
const { clientWidth } = document.documentElement;
|
||||||
|
|
||||||
onRefresh(clientWidth);
|
if (onRefresh) onRefresh(clientWidth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: clientWidth,
|
width: clientWidth,
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
const Sticky: FC<IProps> = ({ children }) => {
|
const Sticky: FC<IProps> = ({ children }) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
let sb = null;
|
let sb;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { ImageGrid } from '../ImageGrid';
|
import { ImageGrid } from '../ImageGrid';
|
||||||
|
@ -8,19 +7,14 @@ import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
|
|
||||||
const mapStateToProps = selectUploads;
|
const mapStateToProps = selectUploads;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
||||||
typeof mapDispatchToProps & {
|
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
temp: string[];
|
|
||||||
setTemp: (val: string[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
||||||
const images = useMemo(
|
const images = useMemo(
|
||||||
|
@ -69,9 +63,6 @@ const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) =
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AudioEditor = connect(
|
const AudioEditor = connect(mapStateToProps, mapDispatchToProps)(AudioEditorUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(AudioEditorUnconnected);
|
|
||||||
|
|
||||||
export { AudioEditor };
|
export { AudioEditor };
|
||||||
|
|
|
@ -35,7 +35,7 @@ const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTitleChange = useCallback(
|
const onTitleChange = useCallback(
|
||||||
(changeId: IFile['id'], title: IFile['metadata']['title']) => {
|
(changeId: IFile['id'], title: string) => {
|
||||||
setFiles(
|
setFiles(
|
||||||
files.map(file =>
|
files.map(file =>
|
||||||
file && file.id === changeId ? { ...file, metadata: { ...file.metadata, title } } : file
|
file && file.id === changeId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC, createElement } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
|
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
|
||||||
|
import { has } from 'ramda';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
data: INode;
|
data: INode;
|
||||||
|
@ -10,13 +11,19 @@ interface IProps {
|
||||||
setTemp: (val: string[]) => void;
|
setTemp: (val: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => (
|
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => {
|
||||||
|
if (!data.type || !has(data.type, NODE_PANEL_COMPONENTS)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
{NODE_PANEL_COMPONENTS[data.type] &&
|
{NODE_PANEL_COMPONENTS[data.type] &&
|
||||||
NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
|
NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
|
||||||
createElement(el, { key, data, setData, temp, setTemp })
|
createElement(el, { key, data, setData, temp, setTemp })
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { EditorPanel };
|
export { EditorPanel };
|
||||||
|
|
|
@ -64,7 +64,10 @@ const EditorUploadButtonUnconnected: FC<IProps> = ({
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const temps = items.map(file => file.temp_id).slice(0, limit);
|
const temps = items
|
||||||
|
.filter(file => file?.temp_id)
|
||||||
|
.map(file => file.temp_id!)
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
setTemp([...temp, ...temps]);
|
setTemp([...temp, ...temps]);
|
||||||
uploadUploadFiles(items);
|
uploadUploadFiles(items);
|
||||||
|
|
|
@ -33,16 +33,16 @@ const EditorUploadCoverButtonUnconnected: FC<IProps> = ({
|
||||||
statuses,
|
statuses,
|
||||||
uploadUploadFiles,
|
uploadUploadFiles,
|
||||||
}) => {
|
}) => {
|
||||||
const [cover_temp, setCoverTemp] = useState<string>(null);
|
const [coverTemp, setCoverTemp] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Object.entries(statuses).forEach(([id, status]) => {
|
Object.entries(statuses).forEach(([id, status]) => {
|
||||||
if (cover_temp === id && !!status.uuid && files[status.uuid]) {
|
if (coverTemp === id && !!status.uuid && files[status.uuid]) {
|
||||||
setData({ ...data, cover: files[status.uuid] });
|
setData({ ...data, cover: files[status.uuid] });
|
||||||
setCoverTemp(null);
|
setCoverTemp('');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [statuses, files, cover_temp, setData, data]);
|
}, [statuses, files, coverTemp, setData, data]);
|
||||||
|
|
||||||
const onUpload = useCallback(
|
const onUpload = useCallback(
|
||||||
(uploads: File[]) => {
|
(uploads: File[]) => {
|
||||||
|
@ -56,7 +56,7 @@ const EditorUploadCoverButtonUnconnected: FC<IProps> = ({
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
setCoverTemp(path([0, 'temp_id'], items));
|
setCoverTemp(path([0, 'temp_id'], items) || '');
|
||||||
uploadUploadFiles(items);
|
uploadUploadFiles(items);
|
||||||
},
|
},
|
||||||
[uploadUploadFiles, setCoverTemp]
|
[uploadUploadFiles, setCoverTemp]
|
||||||
|
@ -73,11 +73,11 @@ const EditorUploadCoverButtonUnconnected: FC<IProps> = ({
|
||||||
[onUpload]
|
[onUpload]
|
||||||
);
|
);
|
||||||
const onDropCover = useCallback(() => {
|
const onDropCover = useCallback(() => {
|
||||||
setData({ ...data, cover: null });
|
setData({ ...data, cover: undefined });
|
||||||
}, [setData, data]);
|
}, [setData, data]);
|
||||||
|
|
||||||
const background = data.cover ? getURL(data.cover, PRESETS['300']) : null;
|
const background = data.cover ? getURL(data.cover, PRESETS['300']) : null;
|
||||||
const status = cover_temp && path([cover_temp], statuses);
|
const status = coverTemp && path([coverTemp], statuses);
|
||||||
const preview = status && path(['preview'], status);
|
const preview = status && path(['preview'], status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,19 +5,14 @@ import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
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 '~/redux/node/types';
|
||||||
|
|
||||||
const mapStateToProps = selectUploads;
|
const mapStateToProps = selectUploads;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
||||||
typeof mapDispatchToProps & {
|
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
temp: string[];
|
|
||||||
setTemp: (val: string[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImageEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
const ImageEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
||||||
const pending_files = useMemo(() => temp.filter(id => !!statuses[id]).map(id => statuses[id]), [
|
const pending_files = useMemo(() => temp.filter(id => !!statuses[id]).map(id => statuses[id]), [
|
||||||
|
@ -34,9 +29,6 @@ const ImageEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) =
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageEditor = connect(
|
const ImageEditor = connect(mapStateToProps, mapDispatchToProps)(ImageEditorUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ImageEditorUnconnected);
|
|
||||||
|
|
||||||
export { ImageEditor };
|
export { ImageEditor };
|
||||||
|
|
|
@ -17,7 +17,7 @@ const SortableAudioGrid = SortableContainer(
|
||||||
items: IFile[];
|
items: IFile[];
|
||||||
locked: IUploadStatus[];
|
locked: IUploadStatus[];
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
onDelete: (file_id: IFile['id']) => void;
|
||||||
onTitleChange: (file_id: IFile['id'], title: IFile['metadata']['title']) => void;
|
onTitleChange: (file_id: IFile['id'], title: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
|
|
|
@ -3,11 +3,9 @@ import { INode } from '~/redux/types';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
|
|
||||||
interface IProps {
|
type IProps = NodeEditorProps & {};
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextEditor: FC<IProps> = ({ data, setData }) => {
|
const TextEditor: FC<IProps> = ({ data, setData }) => {
|
||||||
const setText = useCallback(
|
const setText = useCallback(
|
||||||
|
|
|
@ -5,11 +5,9 @@ import { path } from 'ramda';
|
||||||
import { InputText } from '~/components/input/InputText';
|
import { InputText } from '~/components/input/InputText';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { getYoutubeThumb } from '~/utils/dom';
|
import { getYoutubeThumb } from '~/utils/dom';
|
||||||
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
|
|
||||||
interface IProps {
|
type IProps = NodeEditorProps & {};
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VideoEditor: FC<IProps> = ({ data, setData }) => {
|
const VideoEditor: FC<IProps> = ({ data, setData }) => {
|
||||||
const setUrl = useCallback(
|
const setUrl = useCallback(
|
||||||
|
@ -19,9 +17,10 @@ const VideoEditor: FC<IProps> = ({ data, setData }) => {
|
||||||
|
|
||||||
const url = (path(['blocks', 0, 'url'], data) as string) || '';
|
const url = (path(['blocks', 0, 'url'], data) as string) || '';
|
||||||
const preview = useMemo(() => getYoutubeThumb(url), [url]);
|
const preview = useMemo(() => getYoutubeThumb(url), [url]);
|
||||||
|
const backgroundImage = (preview && `url("${preview}")`) || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.preview} style={{ backgroundImage: preview && `url("${preview}")` }}>
|
<div className={styles.preview} style={{ backgroundImage }}>
|
||||||
<div className={styles.input_wrap}>
|
<div className={styles.input_wrap}>
|
||||||
<div className={classnames(styles.input, { active: !!preview })}>
|
<div className={classnames(styles.input, { active: !!preview })}>
|
||||||
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
|
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
|
||||||
|
|
|
@ -119,7 +119,7 @@ const Cell: FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
const cellText = useMemo(() => formatCellText(text), [text]);
|
const cellText = useMemo(() => formatCellText(text || ''), [text]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.cell, styles[(flow && flow.display) || 'single'])} ref={ref}>
|
<div className={classNames(styles.cell, styles[(flow && flow.display) || 'single'])} ref={ref}>
|
||||||
|
|
|
@ -13,7 +13,12 @@ type IProps = Partial<IFlowState> & {
|
||||||
onChangeCellView: typeof flowSetCellView;
|
onChangeCellView: typeof flowSetCellView;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => (
|
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => {
|
||||||
|
if (!nodes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{nodes.map(node => (
|
{nodes.map(node => (
|
||||||
<Cell
|
<Cell
|
||||||
|
@ -25,4 +30,5 @@ export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { getURL } from '~/utils/dom';
|
||||||
import { withRouter, RouteComponentProps, useHistory } from 'react-router';
|
import { withRouter, RouteComponentProps, useHistory } from 'react-router';
|
||||||
import { URLS, PRESETS } from '~/constants/urls';
|
import { URLS, PRESETS } from '~/constants/urls';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { INode } from "~/redux/types";
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
type IProps = RouteComponentProps & {
|
type IProps = RouteComponentProps & {
|
||||||
heroes: IFlowState['heroes'];
|
heroes: IFlowState['heroes'];
|
||||||
|
@ -18,46 +18,54 @@ const FlowHeroUnconnected: FC<IProps> = ({ heroes }) => {
|
||||||
const [limit, setLimit] = useState(6);
|
const [limit, setLimit] = useState(6);
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const [loaded, setLoaded] = useState<Partial<INode>[]>([]);
|
const [loaded, setLoaded] = useState<Partial<INode>[]>([]);
|
||||||
const timer = useRef(null)
|
const timer = useRef<any>(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onLoad = useCallback((i: number) => {
|
const onLoad = useCallback(
|
||||||
setLoaded([...loaded, heroes[i]])
|
(i: number) => {
|
||||||
}, [heroes, loaded, setLoaded])
|
setLoaded([...loaded, heroes[i]]);
|
||||||
|
},
|
||||||
|
[heroes, loaded, setLoaded]
|
||||||
|
);
|
||||||
|
|
||||||
const items = Math.min(heroes.length, limit)
|
const items = Math.min(heroes.length, limit);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
return loaded[current]?.title || '';
|
return loaded[current]?.title || '';
|
||||||
}, [loaded, current, heroes]);
|
}, [loaded, current, heroes]);
|
||||||
|
|
||||||
const onNext = useCallback(() => {
|
const onNext = useCallback(() => {
|
||||||
if (heroes.length > limit) setLimit(limit + 1)
|
if (heroes.length > limit) setLimit(limit + 1);
|
||||||
setCurrent(current < items - 1 ? current + 1 : 0)
|
setCurrent(current < items - 1 ? current + 1 : 0);
|
||||||
}, [current, items, limit, heroes.length])
|
}, [current, items, limit, heroes.length]);
|
||||||
const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [current, items])
|
const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [
|
||||||
|
current,
|
||||||
|
items,
|
||||||
|
]);
|
||||||
|
|
||||||
const goToNode = useCallback(() => {
|
const goToNode = useCallback(() => {
|
||||||
history.push(URLS.NODE_URL(loaded[current].id))
|
history.push(URLS.NODE_URL(loaded[current].id));
|
||||||
}, [current, loaded]);
|
}, [current, loaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
timer.current = setTimeout(onNext, 5000)
|
timer.current = setTimeout(onNext, 5000);
|
||||||
return () => clearTimeout(timer.current)
|
return () => clearTimeout(timer.current);
|
||||||
}, [current, timer.current])
|
}, [current, timer.current]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded.length === 1) onNext()
|
if (loaded.length === 1) onNext();
|
||||||
}, [loaded])
|
}, [loaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={styles.loaders}>
|
<div className={styles.loaders}>
|
||||||
{
|
{heroes.slice(0, items).map((hero, i) => (
|
||||||
heroes.slice(0, items).map((hero, i) => (
|
<img
|
||||||
<img src={getURL({ url: hero.thumbnail }, preset)} key={hero.id} onLoad={() => onLoad(i)} />
|
src={getURL({ url: hero.thumbnail }, preset)}
|
||||||
))
|
key={hero.id}
|
||||||
}
|
onLoad={() => onLoad(i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loaded.length > 0 && (
|
{loaded.length > 0 && (
|
||||||
|
@ -87,10 +95,7 @@ const FlowHeroUnconnected: FC<IProps> = ({ heroes }) => {
|
||||||
key={hero.id}
|
key={hero.id}
|
||||||
onClick={goToNode}
|
onClick={goToNode}
|
||||||
>
|
>
|
||||||
<img
|
<img src={getURL({ url: hero.thumbnail }, preset)} alt={hero.thumbnail} />
|
||||||
src={getURL({ url: hero.thumbnail }, preset)}
|
|
||||||
alt={hero.thumbnail}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,19 +4,11 @@ import { describeArc } from '~/utils/dom';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
size: number;
|
size: number;
|
||||||
progress: number;
|
progress?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArcProgress: FC<IProps> = ({ size, progress }) => (
|
export const ArcProgress: FC<IProps> = ({ size, progress = 0 }) => (
|
||||||
<svg className={styles.icon} width={size} height={size}>
|
<svg className={styles.icon} width={size} height={size}>
|
||||||
<path
|
<path d={describeArc(size / 2, size / 2, size / 2 - 2, 360 * (1 - progress), 360)} />
|
||||||
d={describeArc(
|
|
||||||
size / 2,
|
|
||||||
size / 2,
|
|
||||||
size / 2 - 2,
|
|
||||||
360 * (1 - progress),
|
|
||||||
360,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Button: FC<IButtonProps> = memo(
|
||||||
ref,
|
ref,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const tooltip = useRef<HTMLSpanElement>();
|
const tooltip = useRef<HTMLSpanElement | null>(null);
|
||||||
const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, {
|
const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
modifiers: [
|
modifiers: [
|
||||||
|
|
|
@ -20,10 +20,16 @@ const InputText: FC<IInputTextProps> = ({
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
const [inner_ref, setInnerRef] = useState<HTMLInputElement>(null);
|
const [inner_ref, setInnerRef] = useState<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
({ target }: ChangeEvent<HTMLInputElement>) => handler(target.value),
|
({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(target.value);
|
||||||
|
},
|
||||||
[handler]
|
[handler]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ export class GodRays extends React.Component<IGodRaysProps> {
|
||||||
|
|
||||||
const ctx = this.canvas.getContext('2d');
|
const ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.globalCompositeOperation = 'luminosity';
|
ctx.globalCompositeOperation = 'luminosity';
|
||||||
ctx.clearRect(0, 0, width, height + 100); // clear canvas
|
ctx.clearRect(0, 0, width, height + 100); // clear canvas
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
@ -123,7 +127,7 @@ export class GodRays extends React.Component<IGodRaysProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement | null | undefined;
|
||||||
|
|
||||||
inc;
|
inc;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,12 @@ const NotificationsUnconnected: FC<IProps> = ({
|
||||||
(notification: INotification) => {
|
(notification: INotification) => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'message':
|
case 'message':
|
||||||
|
if (!(notification as IMessageNotification)?.content?.from?.username) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return authOpenProfile(
|
return authOpenProfile(
|
||||||
(notification as IMessageNotification).content.from.username,
|
(notification as IMessageNotification).content.from!.username,
|
||||||
'messages'
|
'messages'
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
@ -78,9 +82,6 @@ const NotificationsUnconnected: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notifications = connect(
|
const Notifications = connect(mapStateToProps, mapDispatchToProps)(NotificationsUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(NotificationsUnconnected);
|
|
||||||
|
|
||||||
export { Notifications };
|
export { Notifications };
|
||||||
|
|
|
@ -15,10 +15,12 @@ interface IProps {
|
||||||
|
|
||||||
const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, onLogout }) => {
|
const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, onLogout }) => {
|
||||||
const onProfileOpen = useCallback(() => {
|
const onProfileOpen = useCallback(() => {
|
||||||
|
if (!username) return;
|
||||||
authOpenProfile(username, 'profile');
|
authOpenProfile(username, 'profile');
|
||||||
}, [authOpenProfile, username]);
|
}, [authOpenProfile, username]);
|
||||||
|
|
||||||
const onSettingsOpen = useCallback(() => {
|
const onSettingsOpen = useCallback(() => {
|
||||||
|
if (!username) return;
|
||||||
authOpenProfile(username, 'settings');
|
authOpenProfile(username, 'settings');
|
||||||
}, [authOpenProfile, username]);
|
}, [authOpenProfile, username]);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ type Props = ReturnType<typeof mapStateToProps> &
|
||||||
file: IFile;
|
file: IFile;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
onDelete?: (id: IFile['id']) => void;
|
onDelete?: (id: IFile['id']) => void;
|
||||||
onTitleChange?: (file_id: IFile['id'], title: IFile['metadata']['title']) => void;
|
onTitleChange?: (file_id: IFile['id'], title: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AudioPlayerUnconnected = memo(
|
const AudioPlayerUnconnected = memo(
|
||||||
|
@ -93,14 +93,18 @@ const AudioPlayerUnconnected = memo(
|
||||||
[file.metadata]
|
[file.metadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onRename = useCallback((val: string) => onTitleChange(file.id, val), [
|
const onRename = useCallback(
|
||||||
onTitleChange,
|
(val: string) => {
|
||||||
file.id,
|
if (!onTitleChange) return;
|
||||||
]);
|
|
||||||
|
onTitleChange(file.id, val);
|
||||||
|
},
|
||||||
|
[onTitleChange, file.id]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const active = current && current.id === file.id;
|
const active = current && current.id === file.id;
|
||||||
setPlaying(current && current.id === file.id);
|
setPlaying(!!current && current.id === file.id);
|
||||||
|
|
||||||
if (active) Player.on('playprogress', onProgress);
|
if (active) Player.on('playprogress', onProgress);
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,10 @@ const ImageSwitcher: FC<IProps> = ({ total, current, onChange, loaded }) => {
|
||||||
<div className={styles.switcher}>
|
<div className={styles.switcher}>
|
||||||
{range(0, total).map(item => (
|
{range(0, total).map(item => (
|
||||||
<div
|
<div
|
||||||
className={classNames({ is_active: item === current, is_loaded: loaded[item] })}
|
className={classNames({
|
||||||
|
is_active: item === current,
|
||||||
|
is_loaded: loaded && loaded[item],
|
||||||
|
})}
|
||||||
key={item}
|
key={item}
|
||||||
onClick={() => onChange(item)}
|
onClick={() => onChange(item)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comments?: IComment[];
|
comments: IComment[];
|
||||||
count: INodeState['comment_count'];
|
count: INodeState['comment_count'];
|
||||||
user: IUser;
|
user: IUser;
|
||||||
order?: 'ASC' | 'DESC';
|
order?: 'ASC' | 'DESC';
|
||||||
|
|
|
@ -36,8 +36,8 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
const [is_dragging, setIsDragging] = useState(false);
|
const [is_dragging, setIsDragging] = useState(false);
|
||||||
const [drag_start, setDragStart] = useState(0);
|
const [drag_start, setDragStart] = useState(0);
|
||||||
|
|
||||||
const slide = useRef<HTMLDivElement>();
|
const slide = useRef<HTMLDivElement>(null);
|
||||||
const wrap = useRef<HTMLDivElement>();
|
const wrap = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const setHeightThrottled = useCallback(throttle(100, setHeight), [setHeight]);
|
const setHeightThrottled = useCallback(throttle(100, setHeight), [setHeight]);
|
||||||
|
|
||||||
|
@ -221,6 +221,8 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
|
|
||||||
const changeCurrent = useCallback(
|
const changeCurrent = useCallback(
|
||||||
(item: number) => {
|
(item: number) => {
|
||||||
|
if (!wrap.current) return;
|
||||||
|
|
||||||
const { width } = wrap.current.getBoundingClientRect();
|
const { width } = wrap.current.getBoundingClientRect();
|
||||||
setOffset(-1 * item * width);
|
setOffset(-1 * item * width);
|
||||||
},
|
},
|
||||||
|
@ -266,10 +268,10 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
[styles.is_active]: index === current,
|
[styles.is_active]: index === current,
|
||||||
})}
|
})}
|
||||||
ref={setRef(index)}
|
ref={setRef(index)}
|
||||||
key={node.updated_at + file.id}
|
key={`${node?.updated_at || ''} + ${file?.id || ''} + ${index}`}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 0 ${file.metadata.width} ${file.metadata.height}`}
|
viewBox={`0 0 ${file?.metadata?.width || 0} ${file?.metadata?.height || 0}`}
|
||||||
className={classNames(styles.preview, { [styles.is_loaded]: loaded[index] })}
|
className={classNames(styles.preview, { [styles.is_loaded]: loaded[index] })}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: max_height,
|
maxHeight: max_height,
|
||||||
|
|
|
@ -24,11 +24,11 @@ const NodePanel: FC<IProps> = memo(
|
||||||
({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => {
|
({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => {
|
||||||
const [stack, setStack] = useState(false);
|
const [stack, setStack] = useState(false);
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const getPlace = useCallback(() => {
|
const getPlace = useCallback(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
const { bottom } = ref.current.getBoundingClientRect();
|
const { bottom } = ref.current!.getBoundingClientRect();
|
||||||
|
|
||||||
setStack(bottom > window.innerHeight);
|
setStack(bottom > window.innerHeight);
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
@ -75,7 +75,7 @@ const NodePanel: FC<IProps> = memo(
|
||||||
can_edit={can_edit}
|
can_edit={can_edit}
|
||||||
can_like={can_like}
|
can_like={can_like}
|
||||||
can_star={can_star}
|
can_star={can_star}
|
||||||
is_loading={is_loading}
|
is_loading={!!is_loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -96,7 +96,9 @@ const NodePanelInner: FC<IProps> = memo(
|
||||||
<Icon icon="heart" size={24} onClick={onLike} />
|
<Icon icon="heart" size={24} onClick={onLike} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{like_count > 0 && <div className={styles.like_count}>{like_count}</div>}
|
{!!like_count && like_count > 0 && (
|
||||||
|
<div className={styles.like_count}>{like_count}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import styles from "./styles.module.scss";
|
import styles from './styles.module.scss';
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames';
|
||||||
import { INode } from "~/redux/types";
|
import { INode } from '~/redux/types';
|
||||||
import { PRESETS, URLS } from "~/constants/urls";
|
import { PRESETS, URLS } from '~/constants/urls';
|
||||||
import { RouteComponentProps, withRouter } from "react-router";
|
import { RouteComponentProps, withRouter } from 'react-router';
|
||||||
import { getURL, stringToColour } from "~/utils/dom";
|
import { getURL, stringToColour } from '~/utils/dom';
|
||||||
|
|
||||||
type IProps = RouteComponentProps & {
|
type IProps = RouteComponentProps & {
|
||||||
item: Partial<INode>;
|
item: Partial<INode>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CellSize = 'small' | 'medium' | 'large'
|
type CellSize = 'small' | 'medium' | 'large';
|
||||||
|
|
||||||
const getTitleLetters = (title: string): string => {
|
const getTitleLetters = (title: string): string => {
|
||||||
const words = (title && title.split(' ')) || [];
|
const words = (title && title.split(' ')) || [];
|
||||||
|
@ -43,17 +43,21 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const cb = () => setWidth(ref.current.getBoundingClientRect().width)
|
|
||||||
|
const cb = () => setWidth(ref.current!.getBoundingClientRect().width);
|
||||||
|
|
||||||
window.addEventListener('resize', cb);
|
window.addEventListener('resize', cb);
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
|
|
||||||
return () => window.removeEventListener('resize', cb);
|
return () => window.removeEventListener('resize', cb);
|
||||||
}, [ref.current])
|
}, [ref.current]);
|
||||||
|
|
||||||
const size = useMemo<CellSize>(() => {
|
const size = useMemo<CellSize>(() => {
|
||||||
if (width > 90) return 'large';
|
if (width > 90) return 'large';
|
||||||
if (width > 76) return 'medium';
|
if (width > 76) return 'medium';
|
||||||
return 'small';
|
return 'small';
|
||||||
}, [width])
|
}, [width]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -9,7 +9,7 @@ import markdown from '~/styles/common/markdown.module.scss';
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeTextBlock: FC<IProps> = ({ node }) => {
|
const NodeTextBlock: FC<IProps> = ({ node }) => {
|
||||||
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node)), [
|
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [
|
||||||
node.blocks,
|
node.blocks,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeVideoBlock: FC<IProps> = ({ node }) => {
|
const NodeVideoBlock: FC<IProps> = ({ node }) => {
|
||||||
const video = useMemo(() => {
|
const video = useMemo(() => {
|
||||||
const url: string = path(['blocks', 0, 'url'], node);
|
const url: string = path(['blocks', 0, 'url'], node) || '';
|
||||||
const match =
|
const match =
|
||||||
url &&
|
url &&
|
||||||
url.match(
|
url.match(
|
||||||
|
|
|
@ -21,7 +21,7 @@ const NotificationMessage: FC<IProps> = ({
|
||||||
<div className={styles.item} onMouseDown={onMouseDown}>
|
<div className={styles.item} onMouseDown={onMouseDown}>
|
||||||
<div className={styles.item_head}>
|
<div className={styles.item_head}>
|
||||||
<Icon icon="message" />
|
<Icon icon="message" />
|
||||||
<div className={styles.item_title}>Сообщение от ~{from.username}:</div>
|
<div className={styles.item_title}>Сообщение от ~{from?.username}:</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.item_text}>{text}</div>
|
<div className={styles.item_text}>{text}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,7 @@ const MessageFormUnconnected: FC<IProps> = ({
|
||||||
const onSuccess = useCallback(() => {
|
const onSuccess = useCallback(() => {
|
||||||
setText('');
|
setText('');
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing && onCancel) {
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
}, [setText, isEditing, onCancel]);
|
}, [setText, isEditing, onCancel]);
|
||||||
|
@ -50,7 +50,7 @@ const MessageFormUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||||
({ ctrlKey, key }) => {
|
({ ctrlKey, key }) => {
|
||||||
if (!!ctrlKey && key === 'Enter') onSubmit();
|
if (ctrlKey && key === 'Enter') onSubmit();
|
||||||
},
|
},
|
||||||
[onSubmit]
|
[onSubmit]
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,15 +17,15 @@ const ProfileDescriptionUnconnected: FC<IProps> = ({ profile: { user, is_loading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
{user.description && (
|
{!!user?.description && (
|
||||||
<Group
|
<Group
|
||||||
className={styles.content}
|
className={styles.content}
|
||||||
dangerouslySetInnerHTML={{ __html: formatText(user.description) }}
|
dangerouslySetInnerHTML={{ __html: formatText(user.description) }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!user.description && (
|
{!user?.description && (
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
{user.fullname || user.username} пока ничего не рассказал о себе
|
{user?.fullname || user?.username} пока ничего не рассказал о себе
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ITag } from '~/redux/types';
|
||||||
import { TagWrapper } from '~/components/tags/TagWrapper';
|
import { TagWrapper } from '~/components/tags/TagWrapper';
|
||||||
|
|
||||||
const getTagFeature = (tag: Partial<ITag>) => {
|
const getTagFeature = (tag: Partial<ITag>) => {
|
||||||
if (tag.title.substr(0, 1) === '/') return 'green';
|
if (tag?.title?.substr(0, 1) === '/') return 'green';
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
|
@ -87,7 +87,10 @@ const TagAutocompleteUnconnected: FC<Props> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tagSetAutocomplete({ options: [] });
|
tagSetAutocomplete({ options: [] });
|
||||||
return () => tagSetAutocomplete({ options: [] });
|
|
||||||
|
return () => {
|
||||||
|
tagSetAutocomplete({ options: [] });
|
||||||
|
};
|
||||||
}, [tagSetAutocomplete]);
|
}, [tagSetAutocomplete]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -77,6 +77,10 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
const onFocus = useCallback(() => setFocused(true), []);
|
const onFocus = useCallback(() => setFocused(true), []);
|
||||||
const onBlur = useCallback(
|
const onBlur = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
if (!wrapper.current || !ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wrapper.current.contains(event.target)) {
|
if (wrapper.current.contains(event.target)) {
|
||||||
ref.current.focus();
|
ref.current.focus();
|
||||||
return;
|
return;
|
||||||
|
@ -126,7 +130,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
/>
|
/>
|
||||||
</TagWrapper>
|
</TagWrapper>
|
||||||
|
|
||||||
{onInput && focused && input?.length > 0 && (
|
{onInput && focused && input?.length > 0 && ref.current && (
|
||||||
<TagAutocomplete
|
<TagAutocomplete
|
||||||
exclude={exclude}
|
exclude={exclude}
|
||||||
input={ref.current}
|
input={ref.current}
|
||||||
|
|
|
@ -20,14 +20,18 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick,
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(last: string[]) => {
|
(last: string[]) => {
|
||||||
|
if (!onTagsChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const exist = tags.map(tag => tag.title);
|
const exist = tags.map(tag => tag.title);
|
||||||
onTagsChange(uniq([...exist, ...data, ...last]));
|
onTagsChange(uniq([...exist, ...data, ...last]).filter(el => el) as string[]);
|
||||||
},
|
},
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(data.filter(title => !tags.some(tag => tag.title.trim() === title.trim())));
|
setData(data.filter(title => !tags.some(tag => tag?.title?.trim() === title.trim())));
|
||||||
}, [tags]);
|
}, [tags]);
|
||||||
|
|
||||||
const onAppendTag = useCallback(
|
const onAppendTag = useCallback(
|
||||||
|
@ -44,10 +48,10 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick,
|
||||||
return last;
|
return last;
|
||||||
}, [data, setData]);
|
}, [data, setData]);
|
||||||
|
|
||||||
const exclude = useMemo(() => [...(data || []), ...(tags || []).map(({ title }) => title)], [
|
const exclude = useMemo(
|
||||||
data,
|
() => [...(data || []), ...(tags || []).filter(el => el.title).map(({ title }) => title!)],
|
||||||
tags,
|
[data, tags]
|
||||||
]);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagField {...props}>
|
<TagField {...props}>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
export const URLS = {
|
export const URLS = {
|
||||||
BASE: '/',
|
BASE: '/',
|
||||||
BORIS: '/boris',
|
BORIS: '/boris',
|
||||||
|
@ -12,7 +14,7 @@ export const URLS = {
|
||||||
NOT_FOUND: '/lost',
|
NOT_FOUND: '/lost',
|
||||||
BACKEND_DOWN: '/oopsie',
|
BACKEND_DOWN: '/oopsie',
|
||||||
},
|
},
|
||||||
NODE_URL: (id: number | string) => `/post${id}`,
|
NODE_URL: (id: INode['id'] | string) => `/post${id}`,
|
||||||
NODE_TAG_URL: (id: number, tagName: string) => `/post${id}/tag/${tagName}`,
|
NODE_TAG_URL: (id: number, tagName: string) => `/post${id}/tag/${tagName}`,
|
||||||
PROFILE: (username: string) => `/~${username}`,
|
PROFILE: (username: string) => `/~${username}`,
|
||||||
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from "react";
|
import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from 'react';
|
||||||
import styles from "./styles.module.scss";
|
import styles from './styles.module.scss';
|
||||||
import { clearAllBodyScrollLocks, disableBodyScroll } from "body-scroll-lock";
|
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
|
||||||
import { Icon } from "~/components/input/Icon";
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { LoaderCircle } from "~/components/input/LoaderCircle";
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
import { useCloseOnEscape } from "~/utils/hooks";
|
import { useCloseOnEscape } from '~/utils/hooks';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
children: React.ReactChild;
|
children: React.ReactChild;
|
||||||
|
@ -14,7 +14,7 @@ interface IProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
is_loading?: boolean;
|
is_loading?: boolean;
|
||||||
overlay?: ReactElement;
|
overlay?: JSX.Element;
|
||||||
|
|
||||||
onOverlayClick?: MouseEventHandler<HTMLDivElement>;
|
onOverlayClick?: MouseEventHandler<HTMLDivElement>;
|
||||||
onRefCapture?: (ref: any) => void;
|
onRefCapture?: (ref: any) => void;
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import React, { createElement, FC, FormEvent, useCallback, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
createElement,
|
||||||
|
FC,
|
||||||
|
FormEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { IDialogProps } from '~/redux/modal/constants';
|
import { IDialogProps } from '~/redux/modal/constants';
|
||||||
import { useCloseOnEscape } from '~/utils/hooks';
|
import { useCloseOnEscape } from '~/utils/hooks';
|
||||||
|
@ -16,6 +24,7 @@ import { EMPTY_NODE, NODE_EDITORS } from '~/redux/node/constants';
|
||||||
import { BetterScrollDialog } from '../BetterScrollDialog';
|
import { BetterScrollDialog } from '../BetterScrollDialog';
|
||||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
import { has, values } from 'ramda';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const { editor, errors } = selectNode(state);
|
const { editor, errors } = selectNode(state);
|
||||||
|
@ -32,7 +41,7 @@ const mapDispatchToProps = {
|
||||||
type IProps = IDialogProps &
|
type IProps = IDialogProps &
|
||||||
ReturnType<typeof mapStateToProps> &
|
ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {
|
typeof mapDispatchToProps & {
|
||||||
type: keyof typeof NODE_EDITORS;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditorDialogUnconnected: FC<IProps> = ({
|
const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
|
@ -44,7 +53,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState(EMPTY_NODE);
|
const [data, setData] = useState(EMPTY_NODE);
|
||||||
const [temp, setTemp] = useState([]);
|
const [temp, setTemp] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => setData(editor), [editor]);
|
useEffect(() => setData(editor), [editor]);
|
||||||
|
|
||||||
|
@ -93,9 +102,18 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
useCloseOnEscape(onRequestClose);
|
useCloseOnEscape(onRequestClose);
|
||||||
|
|
||||||
const error = errors && Object.values(errors)[0];
|
const error = values(errors)[0];
|
||||||
|
const component = useMemo(() => {
|
||||||
|
if (!has(type, NODE_EDITORS)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(NODE_EDITORS, type)) return null;
|
return NODE_EDITORS[type];
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className={styles.form}>
|
<form onSubmit={onSubmit} className={styles.form}>
|
||||||
|
@ -107,7 +125,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
onClose={onRequestClose}
|
onClose={onRequestClose}
|
||||||
>
|
>
|
||||||
<div className={styles.editor}>
|
<div className={styles.editor}>
|
||||||
{createElement(NODE_EDITORS[type], {
|
{createElement(component, {
|
||||||
data,
|
data,
|
||||||
setData,
|
setData,
|
||||||
temp,
|
temp,
|
||||||
|
|
|
@ -80,7 +80,7 @@ const LoginDialogUnconnected: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) userSetLoginError(null);
|
if (error) userSetLoginError('');
|
||||||
}, [username, password]);
|
}, [username, password]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { Button } from '~/components/input/Button';
|
||||||
import { Grid } from '~/components/containers/Grid';
|
import { Grid } from '~/components/containers/Grid';
|
||||||
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 { ISocialProvider } from '~/redux/auth/types';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
openOauthWindow: (provider: string) => MouseEventHandler;
|
openOauthWindow: (provider: ISocialProvider) => MouseEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginDialogButtons: FC<IProps> = ({ openOauthWindow }) => (
|
const LoginDialogButtons: FC<IProps> = ({ openOauthWindow }) => (
|
||||||
|
|
|
@ -24,7 +24,7 @@ const ModalUnconnected: FC<IProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const onRequestClose = useCallback(() => {
|
const onRequestClose = useCallback(() => {
|
||||||
modalSetShown(false);
|
modalSetShown(false);
|
||||||
modalSetDialog(null);
|
modalSetDialog('');
|
||||||
}, [modalSetShown, modalSetDialog]);
|
}, [modalSetShown, modalSetDialog]);
|
||||||
|
|
||||||
if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null;
|
if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null;
|
||||||
|
@ -43,10 +43,7 @@ const ModalUnconnected: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Modal = connect(
|
const Modal = connect(mapStateToProps, mapDispatchToProps)(ModalUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ModalUnconnected);
|
|
||||||
|
|
||||||
export { ModalUnconnected, Modal };
|
export { ModalUnconnected, Modal };
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,9 @@ const PhotoSwipeUnconnected: FC<Props> = ({ photoswipe, modalSetShown }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.location.hash = 'preview';
|
window.location.hash = 'preview';
|
||||||
return () => (window.location.hash = '');
|
return () => {
|
||||||
|
window.location.hash = '';
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useState, useMemo, useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { IDialogProps } from '~/redux/types';
|
import { IDialogProps } from '~/redux/types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { BetterScrollDialog } from '../BetterScrollDialog';
|
import { BetterScrollDialog } from '../BetterScrollDialog';
|
||||||
|
@ -49,7 +49,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error || is_succesfull) {
|
if (error || is_succesfull) {
|
||||||
authSetRestore({ error: null, is_succesfull: false });
|
authSetRestore({ error: '', is_succesfull: false });
|
||||||
}
|
}
|
||||||
}, [password, password_again]);
|
}, [password, password_again]);
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
|
||||||
<Icon icon="check" size={64} />
|
<Icon icon="check" size={64} />
|
||||||
|
|
||||||
<div>Пароль обновлен</div>
|
<div>Пароль обновлен</div>
|
||||||
<div>Добро пожаловать домой, ~{user.username}!</div>
|
<div>Добро пожаловать домой, ~{user?.username}!</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
|
@ -77,14 +77,16 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
|
||||||
Ура!
|
Ура!
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
) : null,
|
) : (
|
||||||
|
undefined
|
||||||
|
),
|
||||||
[is_succesfull]
|
[is_succesfull]
|
||||||
);
|
);
|
||||||
|
|
||||||
const not_ready = useMemo(() => (is_loading && !user ? <div className={styles.shade} /> : null), [
|
const not_ready = useMemo(
|
||||||
is_loading,
|
() => (is_loading && !user ? <div className={styles.shade} /> : undefined),
|
||||||
user,
|
[is_loading, user]
|
||||||
]);
|
);
|
||||||
|
|
||||||
const invalid_code = useMemo(
|
const invalid_code = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -100,7 +102,9 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
|
||||||
Очень жаль
|
Очень жаль
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
) : null,
|
) : (
|
||||||
|
undefined
|
||||||
|
),
|
||||||
[is_loading, user, error]
|
[is_loading, user, error]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -135,7 +139,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
|
||||||
type="password"
|
type="password"
|
||||||
value={password_again}
|
value={password_again}
|
||||||
handler={setPasswordAgain}
|
handler={setPasswordAgain}
|
||||||
error={password_again && doesnt_match && ERROR_LITERAL[ERRORS.DOESNT_MATCH]}
|
error={password_again && doesnt_match ? ERROR_LITERAL[ERRORS.DOESNT_MATCH] : ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group className={styles.text}>
|
<Group className={styles.text}>
|
||||||
|
|
|
@ -43,7 +43,7 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error || is_succesfull) {
|
if (error || is_succesfull) {
|
||||||
authSetRestore({ error: null, is_succesfull: false });
|
authSetRestore({ error: '', is_succesfull: false });
|
||||||
}
|
}
|
||||||
}, [field]);
|
}, [field]);
|
||||||
|
|
||||||
|
@ -72,7 +72,9 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
|
||||||
Отлично!
|
Отлично!
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
) : null,
|
) : (
|
||||||
|
undefined
|
||||||
|
),
|
||||||
[is_succesfull]
|
[is_succesfull]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user.last_seen_boris &&
|
user.last_seen_boris &&
|
||||||
|
last_comment.created_at &&
|
||||||
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
|
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -12,9 +12,14 @@ import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { NodeRelated } from '~/components/node/NodeRelated';
|
import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
import { NodeTags } from '~/components/node/NodeTags';
|
import { NodeTags } from '~/components/node/NodeTags';
|
||||||
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/redux/node/constants';
|
import {
|
||||||
|
INodeComponentProps,
|
||||||
|
NODE_COMPONENTS,
|
||||||
|
NODE_HEADS,
|
||||||
|
NODE_INLINES,
|
||||||
|
} from '~/redux/node/constants';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { pick } from 'ramda';
|
import { path, pick, prop } from 'ramda';
|
||||||
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
||||||
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
||||||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
|
@ -71,9 +76,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
nodeStar,
|
nodeStar,
|
||||||
nodeLock,
|
nodeLock,
|
||||||
nodeSetCoverImage,
|
nodeSetCoverImage,
|
||||||
nodeLockComment,
|
|
||||||
nodeEditComment,
|
|
||||||
nodeLoadMoreComments,
|
|
||||||
modalShowPhotoswipe,
|
modalShowPhotoswipe,
|
||||||
}) => {
|
}) => {
|
||||||
const [layout, setLayout] = useState({});
|
const [layout, setLayout] = useState({});
|
||||||
|
@ -84,7 +86,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
comments = [],
|
comments = [],
|
||||||
current: node,
|
current: node,
|
||||||
related,
|
related,
|
||||||
comment_data,
|
|
||||||
comment_count,
|
comment_count,
|
||||||
} = useShallowSelect(selectNode);
|
} = useShallowSelect(selectNode);
|
||||||
const updateLayout = useCallback(() => setLayout({}), []);
|
const updateLayout = useCallback(() => setLayout({}), []);
|
||||||
|
@ -103,6 +104,10 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
|
|
||||||
const onTagClick = useCallback(
|
const onTagClick = useCallback(
|
||||||
(tag: Partial<ITag>) => {
|
(tag: Partial<ITag>) => {
|
||||||
|
if (!node?.id || !tag?.title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
|
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
|
||||||
},
|
},
|
||||||
[history, node.id]
|
[history, node.id]
|
||||||
|
@ -112,9 +117,9 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
|
const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
|
||||||
const can_star = useMemo(() => canStarNode(node, user), [node, user]);
|
const can_star = useMemo(() => canStarNode(node, user), [node, user]);
|
||||||
|
|
||||||
const head = node && node.type && NODE_HEADS[node.type];
|
const head = useMemo(() => node?.type && prop(node?.type, NODE_HEADS), [node.type]);
|
||||||
const block = node && node.type && NODE_COMPONENTS[node.type];
|
const block = useMemo(() => node?.type && prop(node?.type, NODE_COMPONENTS), [node.type]);
|
||||||
const inline = node && node.type && NODE_INLINES[node.type];
|
const inline = useMemo(() => node?.type && prop(node?.type, NODE_INLINES), [node.type]);
|
||||||
|
|
||||||
const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]);
|
const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]);
|
||||||
const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]);
|
const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]);
|
||||||
|
@ -147,10 +152,10 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{createNodeBlock(head)}
|
{!!head && createNodeBlock(head)}
|
||||||
|
|
||||||
<Card className={styles.node} seamless>
|
<Card className={styles.node} seamless>
|
||||||
{createNodeBlock(block)}
|
{!!block && createNodeBlock(block)}
|
||||||
|
|
||||||
<NodePanel
|
<NodePanel
|
||||||
node={pick(
|
node={pick(
|
||||||
|
@ -208,12 +213,13 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
{!is_loading &&
|
{!is_loading &&
|
||||||
related &&
|
related &&
|
||||||
related.albums &&
|
related.albums &&
|
||||||
|
!!node?.id &&
|
||||||
Object.keys(related.albums)
|
Object.keys(related.albums)
|
||||||
.filter(album => related.albums[album].length > 0)
|
.filter(album => related.albums[album].length > 0)
|
||||||
.map(album => (
|
.map(album => (
|
||||||
<NodeRelated
|
<NodeRelated
|
||||||
title={
|
title={
|
||||||
<Link to={URLS.NODE_TAG_URL(node.id, encodeURIComponent(album))}>
|
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>
|
||||||
{album}
|
{album}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,42 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from "react";
|
import React, { FC, useCallback, useEffect, useState } 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 { 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 { selectUploads } from '~/redux/uploads/selectors';
|
||||||
import { IFileWithUUID } from "~/redux/types";
|
import { IFileWithUUID } from '~/redux/types';
|
||||||
import uuid from "uuid4";
|
import uuid from 'uuid4';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from "~/redux/uploads/constants";
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
import * as UPLOAD_ACTIONS from "~/redux/uploads/actions";
|
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';
|
||||||
|
|
||||||
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))
|
uploads: pick(['statuses', 'files'], selectUploads(state)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||||
authPatchUser: AUTH_ACTIONS.authPatchUser
|
authPatchUser: AUTH_ACTIONS.authPatchUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
typeof mapDispatchToProps & {};
|
|
||||||
|
|
||||||
const ProfileAvatarUnconnected: FC<IProps> = ({
|
const ProfileAvatarUnconnected: FC<IProps> = ({
|
||||||
user: { id },
|
user: { id },
|
||||||
profile: { is_loading, user },
|
profile: { is_loading, user },
|
||||||
uploads: { statuses, files },
|
uploads: { statuses, files },
|
||||||
uploadUploadFiles,
|
uploadUploadFiles,
|
||||||
authPatchUser
|
authPatchUser,
|
||||||
}) => {
|
}) => {
|
||||||
const can_edit = !is_loading && id && id === user.id;
|
const can_edit = !is_loading && id && id === user?.id;
|
||||||
|
|
||||||
const [temp, setTemp] = useState<string>(null);
|
const [temp, setTemp] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!can_edit) return;
|
if (!can_edit) return;
|
||||||
|
@ -45,7 +44,7 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
|
||||||
Object.entries(statuses).forEach(([id, status]) => {
|
Object.entries(statuses).forEach(([id, status]) => {
|
||||||
if (temp === id && !!status.uuid && files[status.uuid]) {
|
if (temp === id && !!status.uuid && files[status.uuid]) {
|
||||||
authPatchUser({ photo: files[status.uuid] });
|
authPatchUser({ photo: files[status.uuid] });
|
||||||
setTemp(null);
|
setTemp('');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [statuses, files, temp, can_edit, authPatchUser]);
|
}, [statuses, files, temp, can_edit, authPatchUser]);
|
||||||
|
@ -58,11 +57,11 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
|
||||||
temp_id: uuid(),
|
temp_id: uuid(),
|
||||||
subject: UPLOAD_SUBJECTS.AVATAR,
|
subject: UPLOAD_SUBJECTS.AVATAR,
|
||||||
target: UPLOAD_TARGETS.PROFILES,
|
target: UPLOAD_TARGETS.PROFILES,
|
||||||
type: UPLOAD_TYPES.IMAGE
|
type: UPLOAD_TYPES.IMAGE,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
setTemp(path([0, "temp_id"], items));
|
setTemp(path([0, 'temp_id'], items) || '');
|
||||||
uploadUploadFiles(items.slice(0, 1));
|
uploadUploadFiles(items.slice(0, 1));
|
||||||
},
|
},
|
||||||
[uploadUploadFiles, setTemp]
|
[uploadUploadFiles, setTemp]
|
||||||
|
@ -81,13 +80,15 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
|
||||||
[onUpload, can_edit]
|
[onUpload, can_edit]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const backgroundImage = is_loading
|
||||||
|
? undefined
|
||||||
|
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: is_loading
|
backgroundImage,
|
||||||
? null
|
|
||||||
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{can_edit && <input type="file" onInput={onInputChange} />}
|
{can_edit && <input type="file" onInput={onInputChange} />}
|
||||||
|
@ -100,9 +101,6 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileAvatar = connect(
|
const ProfileAvatar = connect(mapStateToProps, mapDispatchToProps)(ProfileAvatarUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ProfileAvatarUnconnected);
|
|
||||||
|
|
||||||
export { ProfileAvatar };
|
export { ProfileAvatar };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
import { IUser } from '~/redux/auth/types';
|
import { IAuthState, IUser } from '~/redux/auth/types';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
@ -14,7 +14,7 @@ interface IProps {
|
||||||
is_loading?: boolean;
|
is_loading?: boolean;
|
||||||
is_own?: boolean;
|
is_own?: boolean;
|
||||||
|
|
||||||
setTab?: (tab: string) => void;
|
setTab?: (tab: IAuthState['profile']['tab']) => void;
|
||||||
|
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
}
|
}
|
||||||
|
@ -26,16 +26,16 @@ const ProfileInfo: FC<IProps> = ({ user, tab, is_loading, is_own, setTab, conten
|
||||||
|
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
{is_loading ? <Placeholder width="80%" /> : user.fullname || user.username}
|
{is_loading ? <Placeholder width="80%" /> : user?.fullname || user?.username}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
{is_loading ? <Placeholder /> : getPrettyDate(user.last_seen)}
|
{is_loading ? <Placeholder /> : getPrettyDate(user?.last_seen)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ProfileTabs tab={tab} is_own={is_own} setTab={setTab} />
|
<ProfileTabs tab={tab} is_own={!!is_own} setTab={setTab} />
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,10 +20,10 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
||||||
const {
|
const {
|
||||||
params: { username },
|
params: { username },
|
||||||
} = useRouteMatch<{ username: string }>();
|
} = useRouteMatch<{ username: string }>();
|
||||||
const [user, setUser] = useState<IUser>(null);
|
const [user, setUser] = useState<IUser | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) setUser(null);
|
if (user) setUser(undefined);
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ const ProfileMessagesUnconnected: FC<IProps> = ({
|
||||||
messagesRefreshMessages,
|
messagesRefreshMessages,
|
||||||
}) => {
|
}) => {
|
||||||
const wasAtBottom = useRef(true);
|
const wasAtBottom = useRef(true);
|
||||||
const [wrap, setWrap] = useState<HTMLDivElement>(null);
|
const [wrap, setWrap] = useState<HTMLDivElement | undefined>(undefined);
|
||||||
const [editingMessageId, setEditingMessageId] = useState(0);
|
const [editingMessageId, setEditingMessageId] = useState(0);
|
||||||
|
|
||||||
const onEditMessage = useCallback((id: number) => setEditingMessageId(id), [setEditingMessageId]);
|
const onEditMessage = useCallback((id: number) => setEditingMessageId(id), [setEditingMessageId]);
|
||||||
|
@ -95,8 +95,11 @@ const ProfileMessagesUnconnected: FC<IProps> = ({
|
||||||
if (!messages.messages.length || profile.is_loading)
|
if (!messages.messages.length || profile.is_loading)
|
||||||
return <NodeNoComments is_loading={messages.is_loading_messages || profile.is_loading} />;
|
return <NodeNoComments is_loading={messages.is_loading_messages || profile.is_loading} />;
|
||||||
|
|
||||||
|
if (messages.messages.length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
messages.messages.length > 0 && (
|
|
||||||
<div className={styles.messages} ref={storeRef}>
|
<div className={styles.messages} ref={storeRef}>
|
||||||
{messages.messages
|
{messages.messages
|
||||||
.filter(message => !!message.text)
|
.filter(message => !!message.text)
|
||||||
|
@ -119,7 +122,6 @@ const ProfileMessagesUnconnected: FC<IProps> = ({
|
||||||
<div className={styles.placeholder}>Когда-нибудь здесь будут еще сообщения</div>
|
<div className={styles.placeholder}>Когда-нибудь здесь будут еще сообщения</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { formatText, getURL } from '~/utils/dom';
|
||||||
import { PRESETS, URLS } from '~/constants/urls';
|
import { PRESETS, URLS } from '~/constants/urls';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
profile: IAuthState['profile'];
|
profile: IAuthState['profile'];
|
||||||
|
@ -26,11 +29,11 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile }) => {
|
||||||
<div className={styles.region_wrap}>
|
<div className={styles.region_wrap}>
|
||||||
<div className={styles.region}>
|
<div className={styles.region}>
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
{profile.is_loading ? <Placeholder /> : profile.user.fullname}
|
{profile.is_loading ? <Placeholder /> : profile?.user?.fullname}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.username}>
|
<div className={styles.username}>
|
||||||
{profile.is_loading ? <Placeholder /> : `~${profile.user.username}`}
|
{profile.is_loading ? <Placeholder /> : `~${profile?.user?.username}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
|
@ -53,7 +56,9 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profile && profile.user && profile.user.description && false && (
|
{profile && profile.user && profile.user.description && false && (
|
||||||
<div className={styles.description}>{profile.user.description}</div>
|
<div className={classNames(styles.description, markdown.wrapper)}>
|
||||||
|
{formatText(profile?.user?.description || '')}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tab: string;
|
tab: string;
|
||||||
is_own: boolean;
|
is_own: boolean;
|
||||||
setTab: (tab: string) => void;
|
setTab?: (tab: IAuthState['profile']['tab']) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
|
const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => {
|
||||||
|
const changeTab = useCallback(
|
||||||
|
(tab: IAuthState['profile']['tab']) => () => {
|
||||||
|
if (!setTab) return;
|
||||||
|
setTab(tab);
|
||||||
|
},
|
||||||
|
[setTab]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'profile' })}
|
className={classNames(styles.tab, { [styles.active]: tab === 'profile' })}
|
||||||
onClick={() => setTab('profile')}
|
onClick={changeTab('profile')}
|
||||||
>
|
>
|
||||||
Профиль
|
Профиль
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'messages' })}
|
className={classNames(styles.tab, { [styles.active]: tab === 'messages' })}
|
||||||
onClick={() => setTab('messages')}
|
onClick={changeTab('messages')}
|
||||||
>
|
>
|
||||||
Сообщения
|
Сообщения
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,13 +36,14 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
|
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
|
||||||
onClick={() => setTab('settings')}
|
onClick={changeTab('settings')}
|
||||||
>
|
>
|
||||||
Настройки
|
Настройки
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { ProfileTabs };
|
export { ProfileTabs };
|
||||||
|
|
|
@ -56,7 +56,7 @@ const ProfileSidebarUnconnected: FC<Props> = ({
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
<div className={classNames(styles.wrap, styles.secondary)}>
|
<div className={classNames(styles.wrap, styles.secondary)}>
|
||||||
<ProfileSidebarInfo is_loading={is_loading} user={user} />
|
{!!user && <ProfileSidebarInfo is_loading={is_loading} user={user} />}
|
||||||
<ProfileSidebarMenu path={url} />
|
<ProfileSidebarMenu path={url} />
|
||||||
<Filler />
|
<Filler />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,7 +35,10 @@ const TagSidebarUnconnected: FC<Props> = ({ nodes, tagLoadNodes, tagSetNodes })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tagLoadNodes(tag);
|
tagLoadNodes(tag);
|
||||||
return () => tagSetNodes({ list: [], count: 0 });
|
|
||||||
|
return () => {
|
||||||
|
tagSetNodes({ list: [], count: 0 });
|
||||||
|
};
|
||||||
}, [tag]);
|
}, [tag]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export type IStatBackend = {
|
||||||
export type IBorisState = Readonly<{
|
export type IBorisState = Readonly<{
|
||||||
stats: {
|
stats: {
|
||||||
git: Partial<IStatGitRow>[];
|
git: Partial<IStatGitRow>[];
|
||||||
backend: IStatBackend;
|
backend?: IStatBackend;
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
@ -39,7 +39,7 @@ export type IBorisState = Readonly<{
|
||||||
const BORIS_INITIAL_STATE: IBorisState = {
|
const BORIS_INITIAL_STATE: IBorisState = {
|
||||||
stats: {
|
stats: {
|
||||||
git: [],
|
git: [],
|
||||||
backend: null,
|
backend: undefined,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({
|
||||||
type: NODE_ACTIONS.SET_SAVE_ERRORS,
|
type: NODE_ACTIONS.SET_SAVE_ERRORS,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeGotoNode = (id: number, node_type: INode['type']) => ({
|
export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({
|
||||||
id,
|
id,
|
||||||
node_type,
|
node_type,
|
||||||
type: NODE_ACTIONS.GOTO_NODE,
|
type: NODE_ACTIONS.GOTO_NODE,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FC } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { IComment, INode, ValueOf } from '../types';
|
import { IComment, INode, ValueOf } from '../types';
|
||||||
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
|
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
|
||||||
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
|
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
|
||||||
|
@ -13,7 +13,7 @@ import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadB
|
||||||
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
|
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
|
||||||
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||||
import { modalShowPhotoswipe } from '../modal/actions';
|
import { modalShowPhotoswipe } from '../modal/actions';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
||||||
import { EditorFiller } from '~/components/editors/EditorFiller';
|
import { EditorFiller } from '~/components/editors/EditorFiller';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
|
@ -50,15 +50,13 @@ export const NODE_ACTIONS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_NODE: INode = {
|
export const EMPTY_NODE: INode = {
|
||||||
id: null,
|
id: 0,
|
||||||
|
user: undefined,
|
||||||
user: null,
|
|
||||||
|
|
||||||
title: '',
|
title: '',
|
||||||
files: [],
|
files: [],
|
||||||
|
|
||||||
cover: null,
|
cover: undefined,
|
||||||
type: null,
|
type: undefined,
|
||||||
|
|
||||||
blocks: [],
|
blocks: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -102,13 +100,16 @@ export const NODE_INLINES: INodeComponents = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_COMMENT: IComment = {
|
export const EMPTY_COMMENT: IComment = {
|
||||||
id: null,
|
id: 0,
|
||||||
text: '',
|
text: '',
|
||||||
files: [],
|
files: [],
|
||||||
user: null,
|
user: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODE_EDITORS = {
|
export const NODE_EDITORS: Record<
|
||||||
|
typeof NODE_TYPES[keyof typeof NODE_TYPES],
|
||||||
|
FC<NodeEditorProps>
|
||||||
|
> = {
|
||||||
[NODE_TYPES.IMAGE]: ImageEditor,
|
[NODE_TYPES.IMAGE]: ImageEditor,
|
||||||
[NODE_TYPES.TEXT]: TextEditor,
|
[NODE_TYPES.TEXT]: TextEditor,
|
||||||
[NODE_TYPES.VIDEO]: VideoEditor,
|
[NODE_TYPES.VIDEO]: VideoEditor,
|
||||||
|
|
|
@ -8,12 +8,12 @@ export type INodeState = Readonly<{
|
||||||
current: INode;
|
current: INode;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
related: {
|
related: {
|
||||||
albums: Record<string, Partial<INode[]>>;
|
albums: Record<string, INode[]>;
|
||||||
similar: Partial<INode[]>;
|
similar: INode[];
|
||||||
};
|
};
|
||||||
comment_data: Record<number, IComment>;
|
comment_data: Record<number, IComment>;
|
||||||
comment_count: number;
|
comment_count: number;
|
||||||
current_cover_image: IFile;
|
current_cover_image?: IFile;
|
||||||
|
|
||||||
error: string;
|
error: string;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
|
@ -38,14 +38,17 @@ const INITIAL_STATE: INodeState = {
|
||||||
},
|
},
|
||||||
comment_count: 0,
|
comment_count: 0,
|
||||||
comments: [],
|
comments: [],
|
||||||
related: null,
|
related: {
|
||||||
current_cover_image: null,
|
albums: {},
|
||||||
|
similar: [],
|
||||||
|
},
|
||||||
|
current_cover_image: undefined,
|
||||||
|
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
is_loading_comments: false,
|
is_loading_comments: false,
|
||||||
is_sending_comment: false,
|
is_sending_comment: false,
|
||||||
|
|
||||||
error: null,
|
error: '',
|
||||||
errors: {},
|
errors: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import { selectNode } from './selectors';
|
||||||
import { Unwrap } from '../types';
|
import { Unwrap } from '../types';
|
||||||
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
|
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
|
||||||
import { DIALOGS } from '~/redux/modal/constants';
|
import { DIALOGS } from '~/redux/modal/constants';
|
||||||
|
import { has } from 'ramda';
|
||||||
|
|
||||||
export function* updateNodeEverywhere(node) {
|
export function* updateNodeEverywhere(node) {
|
||||||
const {
|
const {
|
||||||
|
@ -103,6 +104,9 @@ function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onNodeGoto({ id, node_type }: ReturnType<typeof nodeGotoNode>) {
|
function* onNodeGoto({ id, node_type }: ReturnType<typeof nodeGotoNode>) {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type }));
|
if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type }));
|
||||||
|
|
||||||
yield put(nodeLoadNode(id));
|
yield put(nodeLoadNode(id));
|
||||||
|
@ -224,7 +228,7 @@ function* onUpdateTags({ id, tags }: ReturnType<typeof nodeUpdateTags>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onCreateSaga({ node_type: type }: ReturnType<typeof nodeCreate>) {
|
function* onCreateSaga({ node_type: type }: ReturnType<typeof nodeCreate>) {
|
||||||
if (!NODE_EDITOR_DIALOGS[type]) return;
|
if (!type || !has(type, NODE_EDITOR_DIALOGS)) return;
|
||||||
|
|
||||||
yield put(nodeSetEditor({ ...EMPTY_NODE, ...(NODE_EDITOR_DATA[type] || {}), type }));
|
yield put(nodeSetEditor({ ...EMPTY_NODE, ...(NODE_EDITOR_DATA[type] || {}), type }));
|
||||||
yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type]));
|
yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type]));
|
||||||
|
@ -240,6 +244,8 @@ function* onEditSaga({ id }: ReturnType<typeof nodeEdit>) {
|
||||||
|
|
||||||
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id });
|
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id });
|
||||||
|
|
||||||
|
if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return;
|
||||||
|
|
||||||
if (!NODE_EDITOR_DIALOGS[node?.type]) {
|
if (!NODE_EDITOR_DIALOGS[node?.type]) {
|
||||||
throw new Error('Unknown node type');
|
throw new Error('Unknown node type');
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,3 +83,9 @@ export type ApiLockCommentRequest = {
|
||||||
export type ApiLockcommentResult = {
|
export type ApiLockcommentResult = {
|
||||||
deleted_at: string;
|
deleted_at: string;
|
||||||
};
|
};
|
||||||
|
export type NodeEditorProps = {
|
||||||
|
data: INode;
|
||||||
|
setData: (val: INode) => void;
|
||||||
|
temp: string[];
|
||||||
|
setTemp: (val: string[]) => void;
|
||||||
|
};
|
||||||
|
|
|
@ -71,7 +71,7 @@ export interface IFile {
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
||||||
type: IUploadType;
|
type?: IUploadType;
|
||||||
mime: string;
|
mime: string;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
id3title?: string;
|
id3title?: string;
|
||||||
|
@ -92,7 +92,7 @@ export interface IFileWithUUID {
|
||||||
file: File;
|
file: File;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
target: string;
|
target: string;
|
||||||
type: string;
|
type?: string;
|
||||||
onSuccess?: (file: IFile) => void;
|
onSuccess?: (file: IFile) => void;
|
||||||
onFail?: () => void;
|
onFail?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -111,13 +111,13 @@ export type IBlock = IBlockText | IBlockEmbed;
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
id?: number;
|
id?: number;
|
||||||
user: Partial<IUser>;
|
user?: Partial<IUser>;
|
||||||
|
|
||||||
title: string;
|
title: string;
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
|
|
||||||
cover: IFile;
|
cover?: IFile;
|
||||||
type: string;
|
type?: string;
|
||||||
|
|
||||||
blocks: IBlock[];
|
blocks: IBlock[];
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
|
@ -143,7 +143,7 @@ export interface IComment {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
user: IUser;
|
user?: IUser;
|
||||||
|
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
update_at?: string;
|
update_at?: string;
|
||||||
|
|
|
@ -15,9 +15,9 @@ export const UPLOAD_ACTIONS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_FILE: IFile = {
|
export const EMPTY_FILE: IFile = {
|
||||||
id: null,
|
id: undefined,
|
||||||
user_id: null,
|
user_id: undefined,
|
||||||
node_id: null,
|
node_id: undefined,
|
||||||
|
|
||||||
name: '',
|
name: '',
|
||||||
orig_name: '',
|
orig_name: '',
|
||||||
|
@ -25,21 +25,21 @@ export const EMPTY_FILE: IFile = {
|
||||||
full_path: '',
|
full_path: '',
|
||||||
url: '',
|
url: '',
|
||||||
size: 0,
|
size: 0,
|
||||||
type: null,
|
type: undefined,
|
||||||
mime: '',
|
mime: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
preview: null,
|
preview: '',
|
||||||
error: null,
|
error: '',
|
||||||
uuid: null,
|
uuid: 0,
|
||||||
url: null,
|
url: '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
thumbnail_url: null,
|
thumbnail_url: '',
|
||||||
type: null,
|
type: '',
|
||||||
temp_id: null,
|
temp_id: '',
|
||||||
name: null,
|
name: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// for targeted cancellation
|
// for targeted cancellation
|
||||||
|
|
|
@ -2,38 +2,41 @@ import { assocPath } from 'ramda';
|
||||||
import { omit } from 'ramda';
|
import { omit } from 'ramda';
|
||||||
|
|
||||||
import { UPLOAD_ACTIONS, EMPTY_UPLOAD_STATUS } from './constants';
|
import { UPLOAD_ACTIONS, EMPTY_UPLOAD_STATUS } from './constants';
|
||||||
import {
|
import { uploadAddStatus, uploadDropStatus, uploadSetStatus, uploadAddFile } from './actions';
|
||||||
uploadAddStatus, uploadDropStatus, uploadSetStatus, uploadAddFile
|
|
||||||
} from './actions';
|
|
||||||
import { IUploadState } from './reducer';
|
import { IUploadState } from './reducer';
|
||||||
|
|
||||||
const addStatus = (
|
const addStatus = (
|
||||||
state: IUploadState,
|
state: IUploadState,
|
||||||
{ temp_id, status, }: ReturnType<typeof uploadAddStatus>
|
{ temp_id, status }: ReturnType<typeof uploadAddStatus>
|
||||||
): IUploadState => assocPath(
|
): IUploadState =>
|
||||||
|
assocPath(
|
||||||
['statuses'],
|
['statuses'],
|
||||||
{ ...state.statuses, [temp_id]: { ...EMPTY_UPLOAD_STATUS, ...status, }, },
|
{ ...state.statuses, [temp_id]: { ...EMPTY_UPLOAD_STATUS, ...status } },
|
||||||
state
|
state
|
||||||
);
|
);
|
||||||
|
|
||||||
const dropStatus = (
|
const dropStatus = (
|
||||||
state: IUploadState,
|
state: IUploadState,
|
||||||
{ temp_id, }: ReturnType<typeof uploadDropStatus>
|
{ temp_id }: ReturnType<typeof uploadDropStatus>
|
||||||
): IUploadState => assocPath(['statuses'], omit([temp_id], state.statuses), state);
|
): IUploadState => assocPath(['statuses'], omit([temp_id], state.statuses), state);
|
||||||
|
|
||||||
const setStatus = (
|
const setStatus = (
|
||||||
state: IUploadState,
|
state: IUploadState,
|
||||||
{ temp_id, status, }: ReturnType<typeof uploadSetStatus>
|
{ temp_id, status }: ReturnType<typeof uploadSetStatus>
|
||||||
): IUploadState => assocPath(
|
): IUploadState =>
|
||||||
|
assocPath(
|
||||||
['statuses'],
|
['statuses'],
|
||||||
{
|
{
|
||||||
...state.statuses,
|
...state.statuses,
|
||||||
[temp_id]: { ...(state.statuses[temp_id] || EMPTY_UPLOAD_STATUS), ...status, },
|
[temp_id]: { ...(state.statuses[temp_id] || EMPTY_UPLOAD_STATUS), ...status },
|
||||||
},
|
},
|
||||||
state
|
state
|
||||||
);
|
);
|
||||||
|
|
||||||
const addFile = (state: IUploadState, { file, }: ReturnType<typeof uploadAddFile>): IUploadState => assocPath(['files'], { ...state.files, [file.id]: file, }, 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 = {
|
export const UPLOAD_HANDLERS = {
|
||||||
[UPLOAD_ACTIONS.ADD_STATUS]: addStatus,
|
[UPLOAD_ACTIONS.ADD_STATUS]: addStatus,
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import uuid from 'uuid4';
|
|
||||||
import { IResultWithStatus, IFile, UUID } from '../types';
|
|
||||||
import { HTTP_RESPONSES } from '~/utils/api';
|
|
||||||
import { EMPTY_FILE } from './constants';
|
|
||||||
|
|
||||||
export const uploadMock = ({ temp_id, file }: { temp_id: UUID; file: File }): Promise<IResultWithStatus<IFile>> => (
|
|
||||||
Promise.resolve({
|
|
||||||
status: HTTP_RESPONSES.CREATED,
|
|
||||||
data: {
|
|
||||||
...EMPTY_FILE,
|
|
||||||
id: uuid(),
|
|
||||||
temp_id,
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
}));
|
|
|
@ -73,7 +73,7 @@ function* uploadFile({ file, temp_id, type, target, onSuccess, onFail }: IFileWi
|
||||||
if (!temp_id) return;
|
if (!temp_id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!file.type || !FILE_MIMES[type] || !FILE_MIMES[type].includes(file.type)) {
|
if (!file.type || !type || !FILE_MIMES[type] || !FILE_MIMES[type].includes(file.type)) {
|
||||||
return {
|
return {
|
||||||
error: 'File_Not_Image',
|
error: 'File_Not_Image',
|
||||||
status: HTTP_RESPONSES.BAD_REQUEST,
|
status: HTTP_RESPONSES.BAD_REQUEST,
|
||||||
|
|
|
@ -89,7 +89,10 @@ export const getURLFromString = (
|
||||||
return url.replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT);
|
return url.replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getURL = (file: Partial<IFile>, size?: typeof PRESETS[keyof typeof PRESETS]) => {
|
export const getURL = (
|
||||||
|
file: Partial<IFile> | undefined,
|
||||||
|
size?: typeof PRESETS[keyof typeof PRESETS]
|
||||||
|
) => {
|
||||||
return file?.url ? getURLFromString(file.url, size) : '';
|
return file?.url ? getURLFromString(file.url, size) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,16 +10,20 @@ export const objFromArray = (array: any[], key: string) =>
|
||||||
array.reduce((obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), {});
|
array.reduce((obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), {});
|
||||||
|
|
||||||
export const groupCommentsByUser = (
|
export const groupCommentsByUser = (
|
||||||
result: ICommentGroup[],
|
grouppedComments: ICommentGroup[],
|
||||||
comment: IComment
|
comment: IComment
|
||||||
): ICommentGroup[] => {
|
): ICommentGroup[] => {
|
||||||
const last: ICommentGroup = path([result.length - 1], result) || null;
|
const last: ICommentGroup | undefined = path([grouppedComments.length - 1], grouppedComments);
|
||||||
|
|
||||||
|
if (!comment.user) {
|
||||||
|
return grouppedComments;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(!last || path(['user', 'id'], last) !== path(['user', 'id'], comment)
|
...(!last || path(['user', 'id'], last) !== path(['user', 'id'], comment)
|
||||||
? [
|
? [
|
||||||
// add new group
|
// add new group
|
||||||
...result,
|
...grouppedComments,
|
||||||
{
|
{
|
||||||
user: comment.user,
|
user: comment.user,
|
||||||
comments: [comment],
|
comments: [comment],
|
||||||
|
@ -28,7 +32,7 @@ export const groupCommentsByUser = (
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
// append to last group
|
// append to last group
|
||||||
...result.slice(0, result.length - 1),
|
...grouppedComments.slice(0, grouppedComments.length - 1),
|
||||||
{
|
{
|
||||||
...last,
|
...last,
|
||||||
comments: [...last.comments, comment],
|
comments: [...last.comments, comment],
|
||||||
|
@ -37,6 +41,3 @@ export const groupCommentsByUser = (
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// const isSameComment = (comments, index) =>
|
|
||||||
// comments[index - 1] && comments[index - 1].user.id === comments[index].user.id;
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { IFile, IFileWithUUID } from '~/redux/types';
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||||
import { getFileType } from '~/utils/uploader';
|
import { getFileType } from '~/utils/uploader';
|
||||||
|
@ -7,6 +15,8 @@ import { useDispatch } from 'react-redux';
|
||||||
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
import { has, path } from 'ramda';
|
||||||
|
import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||||
|
|
||||||
export const useFileUploader = (
|
export const useFileUploader = (
|
||||||
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
||||||
|
@ -31,7 +41,7 @@ export const useFileUploader = (
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const temps = items.map(file => file.temp_id);
|
const temps = items.filter(el => !!el.temp_id).map(file => file.temp_id!);
|
||||||
|
|
||||||
setPendingIDs([...pendingIDs, ...temps]);
|
setPendingIDs([...pendingIDs, ...temps]);
|
||||||
dispatch(uploadUploadFiles(items));
|
dispatch(uploadUploadFiles(items));
|
||||||
|
@ -41,9 +51,10 @@ export const useFileUploader = (
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const added = pendingIDs
|
const added = pendingIDs
|
||||||
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
|
.map(temp_uuid => path([temp_uuid, 'uuid'], statuses) as IUploadStatus['uuid'])
|
||||||
.map(el => !!el && uploadedFiles[el])
|
.filter(el => el)
|
||||||
.filter(el => !!el && !files.some(file => file && file.id === el.id));
|
.map(el => (path([String(el)], uploadedFiles) as IFile) || undefined)
|
||||||
|
.filter(el => !!el! && !files.some(file => file && file.id === el.id));
|
||||||
|
|
||||||
const newPending = pendingIDs.filter(
|
const newPending = pendingIDs.filter(
|
||||||
temp_id =>
|
temp_id =>
|
||||||
|
@ -68,7 +79,7 @@ export const useFileUploader = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileUploader = ReturnType<typeof useFileUploader>;
|
export type FileUploader = ReturnType<typeof useFileUploader>;
|
||||||
const FileUploaderContext = createContext<FileUploader>(null);
|
const FileUploaderContext = createContext<FileUploader | undefined>(undefined);
|
||||||
|
|
||||||
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
||||||
value,
|
value,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export const useCloseOnEscape = (onRequestClose: () => void, ignore_inputs = false) => {
|
export const useCloseOnEscape = (onRequestClose?: () => void, ignore_inputs = false) => {
|
||||||
const onEscape = useCallback(
|
const onEscape = useCallback(
|
||||||
event => {
|
event => {
|
||||||
if (event.key !== 'Escape') return;
|
if (event.key !== 'Escape' || !onRequestClose) return;
|
||||||
if (
|
if (
|
||||||
ignore_inputs &&
|
ignore_inputs &&
|
||||||
(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')
|
(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')
|
||||||
|
|
|
@ -12,7 +12,7 @@ const validationSchema = object().shape({
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
||||||
e: string
|
e?: string
|
||||||
) => {
|
) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
|
|
|
@ -32,15 +32,12 @@ export class PlayerClass {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public current: number = 0;
|
public current = 0;
|
||||||
|
public total = 0;
|
||||||
|
public element = new Audio();
|
||||||
|
public duration = 0;
|
||||||
|
|
||||||
public total: number = 0;
|
public set = (src: string) => {
|
||||||
|
|
||||||
public element: HTMLAudioElement = typeof Audio !== 'undefined' ? new Audio() : null;
|
|
||||||
|
|
||||||
public duration: number = 0;
|
|
||||||
|
|
||||||
public set = (src: string): void => {
|
|
||||||
this.element.src = src;
|
this.element.src = src;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { ITag } from '~/redux/types';
|
||||||
export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] =>
|
export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] =>
|
||||||
(tags || []).reduce(
|
(tags || []).reduce(
|
||||||
(obj, tag) =>
|
(obj, tag) =>
|
||||||
tag.title.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]],
|
tag?.title?.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]],
|
||||||
[[], []]
|
[[], []] as Partial<ITag>[][]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const separateTagOptions = (options: string[]): string[][] =>
|
export const separateTagOptions = (options: string[]): string[][] =>
|
||||||
separateTags(options.map((title): Partial<ITag> => ({ title }))).map(item =>
|
separateTags(options.map((title): Partial<ITag> => ({ title }))).map(item =>
|
||||||
item.map(({ title }) => title)
|
item.filter(tag => tag.title).map(({ title }) => title!)
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue