mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
refactored component errors
This commit is contained in:
parent
7031084b09
commit
d4c2e7ee09
79 changed files with 573 additions and 462 deletions
|
@ -33,7 +33,8 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalSho
|
|||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
||||
() =>
|
||||
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
|
||||
),
|
||||
|
|
|
@ -6,6 +6,7 @@ import { selectPlayer } from '~/redux/player/selectors';
|
|||
import { connect } from 'react-redux';
|
||||
import * as PLAYER_ACTIONS from '~/redux/player/actions';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { path } from 'ramda';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
youtubes: selectPlayer(state).youtubes,
|
||||
|
@ -21,30 +22,32 @@ type Props = ReturnType<typeof mapStateToProps> &
|
|||
|
||||
const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||
({ block, youtubes, playerGetYoutubeInfo }) => {
|
||||
const link = useMemo(
|
||||
() =>
|
||||
block.content.match(
|
||||
/https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?([\w\-\=]+)/
|
||||
),
|
||||
[block.content]
|
||||
);
|
||||
const id = useMemo(() => {
|
||||
const match = block.content.match(
|
||||
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-\=]+)/
|
||||
);
|
||||
|
||||
return (match && match[1]) || '';
|
||||
}, [block.content]);
|
||||
|
||||
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!link[5] || youtubes[link[5]]) return;
|
||||
playerGetYoutubeInfo(link[5]);
|
||||
}, [link, playerGetYoutubeInfo]);
|
||||
if (!id) return;
|
||||
playerGetYoutubeInfo(id);
|
||||
}, [id, playerGetYoutubeInfo]);
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
(youtubes[link[5]] && youtubes[link[5]].metadata && youtubes[link[5]].metadata.title) || '',
|
||||
[link, youtubes]
|
||||
);
|
||||
const title = useMemo<string>(() => {
|
||||
if (!id) {
|
||||
return block.content;
|
||||
}
|
||||
|
||||
return path([id, 'metadata', 'title'], youtubes) || block.content;
|
||||
}, [id, youtubes, block.content]);
|
||||
|
||||
return (
|
||||
<div className={styles.embed}>
|
||||
<a href={link[0]} target="_blank" />
|
||||
<a href={id[0]} target="_blank" />
|
||||
|
||||
<div className={styles.preview}>
|
||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||
|
@ -53,7 +56,7 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
|||
<Icon icon="play" size={32} />
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>{title || link[0]}</div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,13 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
|||
|
||||
<Group horizontal className={styles.buttons}>
|
||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
|
||||
|
||||
{!!textarea && (
|
||||
<CommentFormFormatButtons
|
||||
element={textarea}
|
||||
handler={formik.handleChange('text')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading && <LoaderCircle size={20} />}
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
|||
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
||||
|
||||
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), [
|
||||
files,
|
||||
|
@ -70,7 +71,7 @@ const CommentFormAttaches: FC = () => {
|
|||
);
|
||||
|
||||
const onAudioTitleChange = useCallback(
|
||||
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||
(fileId: IFile['id'], title: string) => {
|
||||
setFiles(
|
||||
files.map(file =>
|
||||
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||
|
@ -80,36 +81,36 @@ const CommentFormAttaches: FC = () => {
|
|||
[files, setFiles]
|
||||
);
|
||||
|
||||
return (
|
||||
hasAttaches && (
|
||||
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||
{hasImageAttaches && (
|
||||
<SortableImageGrid
|
||||
onDelete={onFileDelete}
|
||||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={pendingImages}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
/>
|
||||
)}
|
||||
if (!hasAttaches) return null;
|
||||
|
||||
{hasAudioAttaches && (
|
||||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDelete={onFileDelete}
|
||||
onTitleChange={onAudioTitleChange}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={pendingAudios}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||
{hasImageAttaches && (
|
||||
<SortableImageGrid
|
||||
onDelete={onFileDelete}
|
||||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={pendingImages}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasAudioAttaches && (
|
||||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDelete={onFileDelete}
|
||||
onTitleChange={onAudioTitleChange}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={pendingAudios}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ const LocalCommentFormTextarea: FC<IProps> = ({ setRef }) => {
|
|||
|
||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||
({ ctrlKey, key }) => {
|
||||
if (!!ctrlKey && key === 'Enter') handleSubmit(null);
|
||||
if (ctrlKey && key === 'Enter') handleSubmit(undefined);
|
||||
},
|
||||
[handleSubmit]
|
||||
);
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import React, { FC, useState, useCallback, useEffect, useRef } from "react";
|
||||
import { IUser } from "~/redux/auth/types";
|
||||
import React, { FC, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { IUser } from '~/redux/auth/types';
|
||||
import styles from './styles.module.scss';
|
||||
import { getURL } from "~/utils/dom";
|
||||
import { PRESETS } from "~/constants/urls";
|
||||
import classNames from "classnames";
|
||||
import { getURL } from '~/utils/dom';
|
||||
import { PRESETS } from '~/constants/urls';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface IProps {
|
||||
cover: IUser["cover"];
|
||||
cover: IUser['cover'];
|
||||
}
|
||||
|
||||
const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
||||
const ref = useRef<HTMLImageElement>();
|
||||
const ref = useRef<HTMLImageElement>(null);
|
||||
|
||||
const [is_loaded, setIsLoaded] = useState(false);
|
||||
|
||||
|
@ -21,7 +21,7 @@ const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
|||
useEffect(() => {
|
||||
if (!cover || !cover.url || !ref || !ref.current) return;
|
||||
|
||||
ref.current.src = "";
|
||||
ref.current.src = '';
|
||||
setIsLoaded(false);
|
||||
ref.current.src = getURL(cover, PRESETS.cover);
|
||||
}, [cover]);
|
||||
|
|
|
@ -16,7 +16,7 @@ const FullWidth: FC<IProps> = ({ children, onRefresh }) => {
|
|||
const { width } = sample.current.getBoundingClientRect();
|
||||
const { clientWidth } = document.documentElement;
|
||||
|
||||
onRefresh(clientWidth);
|
||||
if (onRefresh) onRefresh(clientWidth);
|
||||
|
||||
return {
|
||||
width: clientWidth,
|
||||
|
|
|
@ -11,7 +11,7 @@ interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
|||
|
||||
const Sticky: FC<IProps> = ({ children }) => {
|
||||
const ref = useRef(null);
|
||||
let sb = null;
|
||||
let sb;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
import { ImageGrid } from '../ImageGrid';
|
||||
|
@ -8,19 +7,14 @@ import { selectUploads } from '~/redux/uploads/selectors';
|
|||
|
||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||
import styles from './styles.module.scss';
|
||||
import { NodeEditorProps } from '~/redux/node/types';
|
||||
|
||||
const mapStateToProps = selectUploads;
|
||||
const mapDispatchToProps = {
|
||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps & {
|
||||
data: INode;
|
||||
setData: (val: INode) => void;
|
||||
temp: string[];
|
||||
setTemp: (val: string[]) => void;
|
||||
};
|
||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
||||
|
||||
const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
||||
const images = useMemo(
|
||||
|
@ -69,9 +63,6 @@ const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) =
|
|||
);
|
||||
};
|
||||
|
||||
const AudioEditor = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AudioEditorUnconnected);
|
||||
const AudioEditor = connect(mapStateToProps, mapDispatchToProps)(AudioEditorUnconnected);
|
||||
|
||||
export { AudioEditor };
|
||||
|
|
|
@ -35,7 +35,7 @@ const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
|||
);
|
||||
|
||||
const onTitleChange = useCallback(
|
||||
(changeId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||
(changeId: IFile['id'], title: string) => {
|
||||
setFiles(
|
||||
files.map(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 { INode } from '~/redux/types';
|
||||
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
|
||||
import { has } from 'ramda';
|
||||
|
||||
interface IProps {
|
||||
data: INode;
|
||||
|
@ -10,13 +11,19 @@ interface IProps {
|
|||
setTemp: (val: string[]) => void;
|
||||
}
|
||||
|
||||
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => (
|
||||
<div className={styles.panel}>
|
||||
{NODE_PANEL_COMPONENTS[data.type] &&
|
||||
NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
|
||||
createElement(el, { key, data, setData, temp, setTemp })
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => {
|
||||
if (!data.type || !has(data.type, NODE_PANEL_COMPONENTS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
{NODE_PANEL_COMPONENTS[data.type] &&
|
||||
NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
|
||||
createElement(el, { key, data, setData, temp, setTemp })
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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]);
|
||||
uploadUploadFiles(items);
|
||||
|
|
|
@ -33,16 +33,16 @@ const EditorUploadCoverButtonUnconnected: FC<IProps> = ({
|
|||
statuses,
|
||||
uploadUploadFiles,
|
||||
}) => {
|
||||
const [cover_temp, setCoverTemp] = useState<string>(null);
|
||||
const [coverTemp, setCoverTemp] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
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] });
|
||||
setCoverTemp(null);
|
||||
setCoverTemp('');
|
||||
}
|
||||
});
|
||||
}, [statuses, files, cover_temp, setData, data]);
|
||||
}, [statuses, files, coverTemp, setData, data]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(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, setCoverTemp]
|
||||
|
@ -73,11 +73,11 @@ const EditorUploadCoverButtonUnconnected: FC<IProps> = ({
|
|||
[onUpload]
|
||||
);
|
||||
const onDropCover = useCallback(() => {
|
||||
setData({ ...data, cover: null });
|
||||
setData({ ...data, cover: undefined });
|
||||
}, [setData, data]);
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,19 +5,14 @@ import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
|||
import { selectUploads } from '~/redux/uploads/selectors';
|
||||
import { ImageGrid } from '~/components/editors/ImageGrid';
|
||||
import styles from './styles.module.scss';
|
||||
import { NodeEditorProps } from '~/redux/node/types';
|
||||
|
||||
const mapStateToProps = selectUploads;
|
||||
const mapDispatchToProps = {
|
||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps & {
|
||||
data: INode;
|
||||
setData: (val: INode) => void;
|
||||
temp: string[];
|
||||
setTemp: (val: string[]) => void;
|
||||
};
|
||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
||||
|
||||
const ImageEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
||||
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(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ImageEditorUnconnected);
|
||||
const ImageEditor = connect(mapStateToProps, mapDispatchToProps)(ImageEditorUnconnected);
|
||||
|
||||
export { ImageEditor };
|
||||
|
|
|
@ -17,7 +17,7 @@ const SortableAudioGrid = SortableContainer(
|
|||
items: IFile[];
|
||||
locked: IUploadStatus[];
|
||||
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 (
|
||||
<div className={styles.grid}>
|
||||
|
|
|
@ -3,11 +3,9 @@ import { INode } from '~/redux/types';
|
|||
import styles from './styles.module.scss';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { path } from 'ramda';
|
||||
import { NodeEditorProps } from '~/redux/node/types';
|
||||
|
||||
interface IProps {
|
||||
data: INode;
|
||||
setData: (val: INode) => void;
|
||||
}
|
||||
type IProps = NodeEditorProps & {};
|
||||
|
||||
const TextEditor: FC<IProps> = ({ data, setData }) => {
|
||||
const setText = useCallback(
|
||||
|
|
|
@ -5,11 +5,9 @@ import { path } from 'ramda';
|
|||
import { InputText } from '~/components/input/InputText';
|
||||
import classnames from 'classnames';
|
||||
import { getYoutubeThumb } from '~/utils/dom';
|
||||
import { NodeEditorProps } from '~/redux/node/types';
|
||||
|
||||
interface IProps {
|
||||
data: INode;
|
||||
setData: (val: INode) => void;
|
||||
}
|
||||
type IProps = NodeEditorProps & {};
|
||||
|
||||
const VideoEditor: FC<IProps> = ({ data, setData }) => {
|
||||
const setUrl = useCallback(
|
||||
|
@ -19,9 +17,10 @@ const VideoEditor: FC<IProps> = ({ data, setData }) => {
|
|||
|
||||
const url = (path(['blocks', 0, 'url'], data) as string) || '';
|
||||
const preview = useMemo(() => getYoutubeThumb(url), [url]);
|
||||
const backgroundImage = (preview && `url("${preview}")`) || '';
|
||||
|
||||
return (
|
||||
<div className={styles.preview} style={{ backgroundImage: preview && `url("${preview}")` }}>
|
||||
<div className={styles.preview} style={{ backgroundImage }}>
|
||||
<div className={styles.input_wrap}>
|
||||
<div className={classnames(styles.input, { active: !!preview })}>
|
||||
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
|
||||
|
|
|
@ -119,7 +119,7 @@ const Cell: FC<IProps> = ({
|
|||
}
|
||||
}, [title]);
|
||||
|
||||
const cellText = useMemo(() => formatCellText(text), [text]);
|
||||
const cellText = useMemo(() => formatCellText(text || ''), [text]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.cell, styles[(flow && flow.display) || 'single'])} ref={ref}>
|
||||
|
|
|
@ -13,16 +13,22 @@ type IProps = Partial<IFlowState> & {
|
|||
onChangeCellView: typeof flowSetCellView;
|
||||
};
|
||||
|
||||
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => (
|
||||
<Fragment>
|
||||
{nodes.map(node => (
|
||||
<Cell
|
||||
key={node.id}
|
||||
node={node}
|
||||
onSelect={onSelect}
|
||||
can_edit={canEditNode(node, user)}
|
||||
onChangeCellView={onChangeCellView}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => {
|
||||
if (!nodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{nodes.map(node => (
|
||||
<Cell
|
||||
key={node.id}
|
||||
node={node}
|
||||
onSelect={onSelect}
|
||||
can_edit={canEditNode(node, user)}
|
||||
onChangeCellView={onChangeCellView}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getURL } from '~/utils/dom';
|
|||
import { withRouter, RouteComponentProps, useHistory } from 'react-router';
|
||||
import { URLS, PRESETS } from '~/constants/urls';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { INode } from "~/redux/types";
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
type IProps = RouteComponentProps & {
|
||||
heroes: IFlowState['heroes'];
|
||||
|
@ -18,46 +18,54 @@ const FlowHeroUnconnected: FC<IProps> = ({ heroes }) => {
|
|||
const [limit, setLimit] = useState(6);
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [loaded, setLoaded] = useState<Partial<INode>[]>([]);
|
||||
const timer = useRef(null)
|
||||
const timer = useRef<any>(null);
|
||||
const history = useHistory();
|
||||
|
||||
const onLoad = useCallback((i: number) => {
|
||||
setLoaded([...loaded, heroes[i]])
|
||||
}, [heroes, loaded, setLoaded])
|
||||
const onLoad = useCallback(
|
||||
(i: number) => {
|
||||
setLoaded([...loaded, heroes[i]]);
|
||||
},
|
||||
[heroes, loaded, setLoaded]
|
||||
);
|
||||
|
||||
const items = Math.min(heroes.length, limit)
|
||||
const items = Math.min(heroes.length, limit);
|
||||
|
||||
const title = useMemo(() => {
|
||||
return loaded[current]?.title || '';
|
||||
}, [loaded, current, heroes]);
|
||||
|
||||
const onNext = useCallback(() => {
|
||||
if (heroes.length > limit) setLimit(limit + 1)
|
||||
setCurrent(current < items - 1 ? current + 1 : 0)
|
||||
}, [current, items, limit, heroes.length])
|
||||
const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [current, items])
|
||||
if (heroes.length > limit) setLimit(limit + 1);
|
||||
setCurrent(current < items - 1 ? current + 1 : 0);
|
||||
}, [current, items, limit, heroes.length]);
|
||||
const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [
|
||||
current,
|
||||
items,
|
||||
]);
|
||||
|
||||
const goToNode = useCallback(() => {
|
||||
history.push(URLS.NODE_URL(loaded[current].id))
|
||||
history.push(URLS.NODE_URL(loaded[current].id));
|
||||
}, [current, loaded]);
|
||||
|
||||
useEffect(() => {
|
||||
timer.current = setTimeout(onNext, 5000)
|
||||
return () => clearTimeout(timer.current)
|
||||
}, [current, timer.current])
|
||||
timer.current = setTimeout(onNext, 5000);
|
||||
return () => clearTimeout(timer.current);
|
||||
}, [current, timer.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded.length === 1) onNext()
|
||||
}, [loaded])
|
||||
if (loaded.length === 1) onNext();
|
||||
}, [loaded]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.loaders}>
|
||||
{
|
||||
heroes.slice(0, items).map((hero, i) => (
|
||||
<img src={getURL({ url: hero.thumbnail }, preset)} key={hero.id} onLoad={() => onLoad(i)} />
|
||||
))
|
||||
}
|
||||
{heroes.slice(0, items).map((hero, i) => (
|
||||
<img
|
||||
src={getURL({ url: hero.thumbnail }, preset)}
|
||||
key={hero.id}
|
||||
onLoad={() => onLoad(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loaded.length > 0 && (
|
||||
|
@ -87,10 +95,7 @@ const FlowHeroUnconnected: FC<IProps> = ({ heroes }) => {
|
|||
key={hero.id}
|
||||
onClick={goToNode}
|
||||
>
|
||||
<img
|
||||
src={getURL({ url: hero.thumbnail }, preset)}
|
||||
alt={hero.thumbnail}
|
||||
/>
|
||||
<img src={getURL({ url: hero.thumbnail }, preset)} alt={hero.thumbnail} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -4,19 +4,11 @@ import { describeArc } from '~/utils/dom';
|
|||
|
||||
interface IProps {
|
||||
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}>
|
||||
<path
|
||||
d={describeArc(
|
||||
size / 2,
|
||||
size / 2,
|
||||
size / 2 - 2,
|
||||
360 * (1 - progress),
|
||||
360,
|
||||
)}
|
||||
/>
|
||||
<path d={describeArc(size / 2, size / 2, size / 2 - 2, 360 * (1 - progress), 360)} />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const Button: FC<IButtonProps> = memo(
|
|||
ref,
|
||||
...props
|
||||
}) => {
|
||||
const tooltip = useRef<HTMLSpanElement>();
|
||||
const tooltip = useRef<HTMLSpanElement | null>(null);
|
||||
const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, {
|
||||
placement: 'top',
|
||||
modifiers: [
|
||||
|
|
|
@ -20,10 +20,16 @@ const InputText: FC<IInputTextProps> = ({
|
|||
...props
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [inner_ref, setInnerRef] = useState<HTMLInputElement>(null);
|
||||
const [inner_ref, setInnerRef] = useState<HTMLInputElement | null>(null);
|
||||
|
||||
const onInput = useCallback(
|
||||
({ target }: ChangeEvent<HTMLInputElement>) => handler(target.value),
|
||||
({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(target.value);
|
||||
},
|
||||
[handler]
|
||||
);
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ export class GodRays extends React.Component<IGodRaysProps> {
|
|||
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.globalCompositeOperation = 'luminosity';
|
||||
ctx.clearRect(0, 0, width, height + 100); // clear canvas
|
||||
ctx.save();
|
||||
|
@ -123,7 +127,7 @@ export class GodRays extends React.Component<IGodRaysProps> {
|
|||
);
|
||||
}
|
||||
|
||||
canvas: HTMLCanvasElement;
|
||||
canvas: HTMLCanvasElement | null | undefined;
|
||||
|
||||
inc;
|
||||
}
|
||||
|
|
|
@ -42,8 +42,12 @@ const NotificationsUnconnected: FC<IProps> = ({
|
|||
(notification: INotification) => {
|
||||
switch (notification.type) {
|
||||
case 'message':
|
||||
if (!(notification as IMessageNotification)?.content?.from?.username) {
|
||||
return;
|
||||
}
|
||||
|
||||
return authOpenProfile(
|
||||
(notification as IMessageNotification).content.from.username,
|
||||
(notification as IMessageNotification).content.from!.username,
|
||||
'messages'
|
||||
);
|
||||
default:
|
||||
|
@ -78,9 +82,6 @@ const NotificationsUnconnected: FC<IProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const Notifications = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(NotificationsUnconnected);
|
||||
const Notifications = connect(mapStateToProps, mapDispatchToProps)(NotificationsUnconnected);
|
||||
|
||||
export { Notifications };
|
||||
|
|
|
@ -15,10 +15,12 @@ interface IProps {
|
|||
|
||||
const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, onLogout }) => {
|
||||
const onProfileOpen = useCallback(() => {
|
||||
if (!username) return;
|
||||
authOpenProfile(username, 'profile');
|
||||
}, [authOpenProfile, username]);
|
||||
|
||||
const onSettingsOpen = useCallback(() => {
|
||||
if (!username) return;
|
||||
authOpenProfile(username, 'settings');
|
||||
}, [authOpenProfile, username]);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ type Props = ReturnType<typeof mapStateToProps> &
|
|||
file: IFile;
|
||||
isEditing?: boolean;
|
||||
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(
|
||||
|
@ -93,14 +93,18 @@ const AudioPlayerUnconnected = memo(
|
|||
[file.metadata]
|
||||
);
|
||||
|
||||
const onRename = useCallback((val: string) => onTitleChange(file.id, val), [
|
||||
onTitleChange,
|
||||
file.id,
|
||||
]);
|
||||
const onRename = useCallback(
|
||||
(val: string) => {
|
||||
if (!onTitleChange) return;
|
||||
|
||||
onTitleChange(file.id, val);
|
||||
},
|
||||
[onTitleChange, file.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
||||
|
|
|
@ -19,7 +19,10 @@ const ImageSwitcher: FC<IProps> = ({ total, current, onChange, loaded }) => {
|
|||
<div className={styles.switcher}>
|
||||
{range(0, total).map(item => (
|
||||
<div
|
||||
className={classNames({ is_active: item === current, is_loaded: loaded[item] })}
|
||||
className={classNames({
|
||||
is_active: item === current,
|
||||
is_loaded: loaded && loaded[item],
|
||||
})}
|
||||
key={item}
|
||||
onClick={() => onChange(item)}
|
||||
/>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
interface IProps {
|
||||
comments?: IComment[];
|
||||
comments: IComment[];
|
||||
count: INodeState['comment_count'];
|
||||
user: IUser;
|
||||
order?: 'ASC' | 'DESC';
|
||||
|
|
|
@ -36,8 +36,8 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
|||
const [is_dragging, setIsDragging] = useState(false);
|
||||
const [drag_start, setDragStart] = useState(0);
|
||||
|
||||
const slide = useRef<HTMLDivElement>();
|
||||
const wrap = useRef<HTMLDivElement>();
|
||||
const slide = useRef<HTMLDivElement>(null);
|
||||
const wrap = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setHeightThrottled = useCallback(throttle(100, setHeight), [setHeight]);
|
||||
|
||||
|
@ -221,6 +221,8 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
|||
|
||||
const changeCurrent = useCallback(
|
||||
(item: number) => {
|
||||
if (!wrap.current) return;
|
||||
|
||||
const { width } = wrap.current.getBoundingClientRect();
|
||||
setOffset(-1 * item * width);
|
||||
},
|
||||
|
@ -266,10 +268,10 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
|||
[styles.is_active]: index === current,
|
||||
})}
|
||||
ref={setRef(index)}
|
||||
key={node.updated_at + file.id}
|
||||
key={`${node?.updated_at || ''} + ${file?.id || ''} + ${index}`}
|
||||
>
|
||||
<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] })}
|
||||
style={{
|
||||
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 }) => {
|
||||
const [stack, setStack] = useState(false);
|
||||
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const getPlace = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { bottom } = ref.current.getBoundingClientRect();
|
||||
const { bottom } = ref.current!.getBoundingClientRect();
|
||||
|
||||
setStack(bottom > window.innerHeight);
|
||||
}, [ref]);
|
||||
|
@ -75,7 +75,7 @@ const NodePanel: FC<IProps> = memo(
|
|||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
is_loading={is_loading}
|
||||
is_loading={!!is_loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -96,7 +96,9 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
<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>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import styles from "./styles.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { INode } from "~/redux/types";
|
||||
import { PRESETS, URLS } from "~/constants/urls";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { getURL, stringToColour } from "~/utils/dom";
|
||||
import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
import classNames from 'classnames';
|
||||
import { INode } from '~/redux/types';
|
||||
import { PRESETS, URLS } from '~/constants/urls';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { getURL, stringToColour } from '~/utils/dom';
|
||||
|
||||
type IProps = RouteComponentProps & {
|
||||
item: Partial<INode>;
|
||||
};
|
||||
|
||||
type CellSize = 'small' | 'medium' | 'large'
|
||||
type CellSize = 'small' | 'medium' | 'large';
|
||||
|
||||
const getTitleLetters = (title: string): string => {
|
||||
const words = (title && title.split(' ')) || [];
|
||||
|
@ -43,17 +43,21 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const cb = () => setWidth(ref.current.getBoundingClientRect().width)
|
||||
|
||||
const cb = () => setWidth(ref.current!.getBoundingClientRect().width);
|
||||
|
||||
window.addEventListener('resize', cb);
|
||||
|
||||
cb();
|
||||
|
||||
return () => window.removeEventListener('resize', cb);
|
||||
}, [ref.current])
|
||||
}, [ref.current]);
|
||||
|
||||
const size = useMemo<CellSize>(() => {
|
||||
if (width > 90) return 'large';
|
||||
if (width > 76) return 'medium';
|
||||
return 'small';
|
||||
}, [width])
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -9,7 +9,7 @@ import markdown from '~/styles/common/markdown.module.scss';
|
|||
interface IProps extends INodeComponentProps {}
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ interface IProps extends INodeComponentProps {}
|
|||
|
||||
const NodeVideoBlock: FC<IProps> = ({ node }) => {
|
||||
const video = useMemo(() => {
|
||||
const url: string = path(['blocks', 0, 'url'], node);
|
||||
const url: string = path(['blocks', 0, 'url'], node) || '';
|
||||
const match =
|
||||
url &&
|
||||
url.match(
|
||||
|
|
|
@ -21,7 +21,7 @@ const NotificationMessage: FC<IProps> = ({
|
|||
<div className={styles.item} onMouseDown={onMouseDown}>
|
||||
<div className={styles.item_head}>
|
||||
<Icon icon="message" />
|
||||
<div className={styles.item_title}>Сообщение от ~{from.username}:</div>
|
||||
<div className={styles.item_title}>Сообщение от ~{from?.username}:</div>
|
||||
</div>
|
||||
<div className={styles.item_text}>{text}</div>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,7 @@ const MessageFormUnconnected: FC<IProps> = ({
|
|||
const onSuccess = useCallback(() => {
|
||||
setText('');
|
||||
|
||||
if (isEditing) {
|
||||
if (isEditing && onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}, [setText, isEditing, onCancel]);
|
||||
|
@ -50,7 +50,7 @@ const MessageFormUnconnected: FC<IProps> = ({
|
|||
|
||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||
({ ctrlKey, key }) => {
|
||||
if (!!ctrlKey && key === 'Enter') onSubmit();
|
||||
if (ctrlKey && key === 'Enter') onSubmit();
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
|
|
@ -17,15 +17,15 @@ const ProfileDescriptionUnconnected: FC<IProps> = ({ profile: { user, is_loading
|
|||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{user.description && (
|
||||
{!!user?.description && (
|
||||
<Group
|
||||
className={styles.content}
|
||||
dangerouslySetInnerHTML={{ __html: formatText(user.description) }}
|
||||
/>
|
||||
)}
|
||||
{!user.description && (
|
||||
{!user?.description && (
|
||||
<div className={styles.placeholder}>
|
||||
{user.fullname || user.username} пока ничего не рассказал о себе
|
||||
{user?.fullname || user?.username} пока ничего не рассказал о себе
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ITag } from '~/redux/types';
|
|||
import { TagWrapper } from '~/components/tags/TagWrapper';
|
||||
|
||||
const getTagFeature = (tag: Partial<ITag>) => {
|
||||
if (tag.title.substr(0, 1) === '/') return 'green';
|
||||
if (tag?.title?.substr(0, 1) === '/') return 'green';
|
||||
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -87,7 +87,10 @@ const TagAutocompleteUnconnected: FC<Props> = ({
|
|||
|
||||
useEffect(() => {
|
||||
tagSetAutocomplete({ options: [] });
|
||||
return () => tagSetAutocomplete({ options: [] });
|
||||
|
||||
return () => {
|
||||
tagSetAutocomplete({ options: [] });
|
||||
};
|
||||
}, [tagSetAutocomplete]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -77,6 +77,10 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
const onFocus = useCallback(() => setFocused(true), []);
|
||||
const onBlur = useCallback(
|
||||
event => {
|
||||
if (!wrapper.current || !ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wrapper.current.contains(event.target)) {
|
||||
ref.current.focus();
|
||||
return;
|
||||
|
@ -126,7 +130,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
/>
|
||||
</TagWrapper>
|
||||
|
||||
{onInput && focused && input?.length > 0 && (
|
||||
{onInput && focused && input?.length > 0 && ref.current && (
|
||||
<TagAutocomplete
|
||||
exclude={exclude}
|
||||
input={ref.current}
|
||||
|
|
|
@ -20,14 +20,18 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick,
|
|||
|
||||
const onSubmit = useCallback(
|
||||
(last: string[]) => {
|
||||
if (!onTagsChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exist = tags.map(tag => tag.title);
|
||||
onTagsChange(uniq([...exist, ...data, ...last]));
|
||||
onTagsChange(uniq([...exist, ...data, ...last]).filter(el => el) as string[]);
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const onAppendTag = useCallback(
|
||||
|
@ -44,10 +48,10 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick,
|
|||
return last;
|
||||
}, [data, setData]);
|
||||
|
||||
const exclude = useMemo(() => [...(data || []), ...(tags || []).map(({ title }) => title)], [
|
||||
data,
|
||||
tags,
|
||||
]);
|
||||
const exclude = useMemo(
|
||||
() => [...(data || []), ...(tags || []).filter(el => el.title).map(({ title }) => title!)],
|
||||
[data, tags]
|
||||
);
|
||||
|
||||
return (
|
||||
<TagField {...props}>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue