mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-30 15:16:41 +07:00
refactored component errors
This commit is contained in:
parent
7031084b09
commit
d4c2e7ee09
79 changed files with 573 additions and 462 deletions
src
components
comment
CommentContent
CommentEmbedBlock
CommentForm
CommentFormAttaches
LocalCommentFormTextarea
containers
editors
AudioEditor
AudioGrid
EditorPanel
EditorUploadButton
EditorUploadCoverButton
ImageEditor
SortableAudioGrid
TextEditor
VideoEditor
flow
input
main
media/AudioPlayer
node
ImageSwitcher
NodeComments
NodeImageSlideBlock
NodePanel
NodePanelInner
NodeRelatedItem
NodeTextBlock
NodeVideoBlock
notifications/NotificationMessage
profile
tags
constants
containers
dialogs
BetterScrollDialog
EditorDialog
LoginDialog
LoginDialogButtons
Modal
PhotoSwipe
RestorePasswordDialog
RestoreRequestDialog
node
profile
ProfileAvatar
ProfileInfo
ProfileLayout
ProfileMessages
ProfilePageLeft
ProfileTabs
sidebars
redux
utils
|
@ -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,7 +11,12 @@ 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) =>
|
||||||
|
@ -18,5 +24,6 @@ const EditorPanel: FC<IProps> = ({ 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
|
||||||
|
@ -26,3 +31,4 @@ 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,7 +36,7 @@ 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>
|
||||||
|
@ -34,5 +44,6 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
|
||||||
)}
|
)}
|
||||||
</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