1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-30 15:16:41 +07:00

refactored component errors

This commit is contained in:
Fedor Katurov 2021-03-03 17:54:58 +07:00
parent 7031084b09
commit d4c2e7ee09
79 changed files with 573 additions and 462 deletions
src
components
comment
CommentContent
CommentEmbedBlock
CommentForm
CommentFormAttaches
LocalCommentFormTextarea
containers
CoverBackdrop
FullWidth
Sticky
editors
AudioEditor
AudioGrid
EditorPanel
EditorUploadButton
EditorUploadCoverButton
ImageEditor
SortableAudioGrid
TextEditor
VideoEditor
flow
Cell
FlowGrid
FlowHero
input
ArcProgress
Button
InputText
main
GodRays
Notifications
UserButton
media/AudioPlayer
node
ImageSwitcher
NodeComments
NodeImageSlideBlock
NodePanel
NodePanelInner
NodeRelatedItem
NodeTextBlock
NodeVideoBlock
notifications/NotificationMessage
profile
MessageForm
ProfileDescription
tags
Tag
TagAutocomplete
TagInput
Tags
constants
containers
dialogs
BetterScrollDialog
EditorDialog
LoginDialog
LoginDialogButtons
Modal
PhotoSwipe
RestorePasswordDialog
RestoreRequestDialog
node
BorisLayout
NodeLayout
profile
ProfileAvatar
ProfileInfo
ProfileLayout
ProfileMessages
ProfilePageLeft
ProfileTabs
sidebars
ProfileSidebar
TagSidebar
redux
utils

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,7 +11,12 @@ interface IProps {
setTemp: (val: string[]) => void; setTemp: (val: string[]) => void;
} }
const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => ( const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => {
if (!data.type || !has(data.type, NODE_PANEL_COMPONENTS)) {
return null;
}
return (
<div className={styles.panel}> <div className={styles.panel}>
{NODE_PANEL_COMPONENTS[data.type] && {NODE_PANEL_COMPONENTS[data.type] &&
NODE_PANEL_COMPONENTS[data.type].map((el, key) => NODE_PANEL_COMPONENTS[data.type].map((el, key) =>
@ -18,5 +24,6 @@ const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => (
)} )}
</div> </div>
); );
};
export { EditorPanel }; export { EditorPanel };

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
@ -26,3 +31,4 @@ 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,7 +36,7 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
<> <>
<div <div
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })} className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
onClick={() => setTab('settings')} onClick={changeTab('settings')}
> >
Настройки Настройки
</div> </div>
@ -34,5 +44,6 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => (
)} )}
</div> </div>
); );
};
export { ProfileTabs }; export { ProfileTabs };

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