1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

refactored component errors

This commit is contained in:
Fedor Katurov 2021-03-03 17:54:58 +07:00
parent 7031084b09
commit d4c2e7ee09
79 changed files with 573 additions and 462 deletions

View file

@ -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
), ),

View file

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

View file

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

View file

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

View file

@ -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]
); );

View file

@ -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]);

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import React, { FC, createElement } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants'; import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
import { has } from 'ramda';
interface IProps { interface IProps {
data: INode; data: INode;
@ -10,13 +11,19 @@ interface IProps {
setTemp: (val: string[]) => void; setTemp: (val: string[]) => void;
} }
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => ( const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => {
if (!data.type || !has(data.type, NODE_PANEL_COMPONENTS)) {
return null;
}
return (
<div className={styles.panel}> <div className={styles.panel}>
{NODE_PANEL_COMPONENTS[data.type] && {NODE_PANEL_COMPONENTS[data.type] &&
NODE_PANEL_COMPONENTS[data.type].map((el, key) => NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
createElement(el, { key, data, setData, temp, setTemp }) createElement(el, { key, data, setData, temp, setTemp })
)} )}
</div> </div>
); );
};
export { EditorPanel }; export { EditorPanel };

View file

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

View file

@ -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 (

View file

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

View file

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

View file

@ -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(

View file

@ -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="Адрес видео" />

View file

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

View file

@ -13,7 +13,12 @@ type IProps = Partial<IFlowState> & {
onChangeCellView: typeof flowSetCellView; onChangeCellView: typeof flowSetCellView;
}; };
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => ( export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => {
if (!nodes) {
return null;
}
return (
<Fragment> <Fragment>
{nodes.map(node => ( {nodes.map(node => (
<Cell <Cell
@ -25,4 +30,5 @@ export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }
/> />
))} ))}
</Fragment> </Fragment>
); );
};

View file

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

View file

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

View file

@ -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: [

View file

@ -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]
); );

View file

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

View file

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

View file

@ -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]);

View file

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

View file

@ -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)}
/> />

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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,
]); ]);

View file

@ -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(

View file

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

View file

@ -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]
); );

View file

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

View file

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

View file

@ -87,7 +87,10 @@ const TagAutocompleteUnconnected: FC<Props> = ({
useEffect(() => { useEffect(() => {
tagSetAutocomplete({ options: [] }); tagSetAutocomplete({ options: [] });
return () => tagSetAutocomplete({ options: [] });
return () => {
tagSetAutocomplete({ options: [] });
};
}, [tagSetAutocomplete]); }, [tagSetAutocomplete]);
useEffect(() => { useEffect(() => {

View file

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

View file

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

View file

@ -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}`,

View file

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

View file

@ -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,

View file

@ -80,7 +80,7 @@ const LoginDialogUnconnected: FC<IProps> = ({
); );
useEffect(() => { useEffect(() => {
if (error) userSetLoginError(null); if (error) userSetLoginError('');
}, [username, password]); }, [username, password]);
useEffect(() => { useEffect(() => {

View file

@ -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 }) => (

View file

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

View file

@ -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 (

View file

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

View file

@ -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]
); );

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {

View file

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

View file

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

View file

@ -1,24 +1,34 @@
import React, { FC } from 'react'; import React, { FC, useCallback } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import classNames from 'classnames'; import classNames from 'classnames';
import { IAuthState } from '~/redux/auth/types';
interface IProps { interface IProps {
tab: string; tab: string;
is_own: boolean; is_own: boolean;
setTab: (tab: string) => void; setTab?: (tab: IAuthState['profile']['tab']) => void;
} }
const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => ( const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => {
const changeTab = useCallback(
(tab: IAuthState['profile']['tab']) => () => {
if (!setTab) return;
setTab(tab);
},
[setTab]
);
return (
<div className={styles.wrap}> <div className={styles.wrap}>
<div <div
className={classNames(styles.tab, { [styles.active]: tab === 'profile' })} className={classNames(styles.tab, { [styles.active]: tab === 'profile' })}
onClick={() => setTab('profile')} onClick={changeTab('profile')}
> >
Профиль Профиль
</div> </div>
<div <div
className={classNames(styles.tab, { [styles.active]: tab === 'messages' })} className={classNames(styles.tab, { [styles.active]: tab === 'messages' })}
onClick={() => setTab('messages')} onClick={changeTab('messages')}
> >
Сообщения Сообщения
</div> </div>
@ -26,13 +36,14 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
<> <>
<div <div
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })} className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
onClick={() => setTab('settings')} onClick={changeTab('settings')}
> >
Настройки Настройки
</div> </div>
</> </>
)} )}
</div> </div>
); );
};
export { ProfileTabs }; export { ProfileTabs };

View file

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

View file

@ -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(() => {

View file

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

View file

@ -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,

View file

@ -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,

View file

@ -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: {},
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -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,

View file

@ -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) : '';
}; };

View file

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

View file

@ -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,

View file

@ -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')

View file

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

View file

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

View file

@ -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!)
); );