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

refactored component errors

This commit is contained in:
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

@ -1,9 +1,9 @@
import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from "react";
import styles from "./styles.module.scss";
import { clearAllBodyScrollLocks, disableBodyScroll } from "body-scroll-lock";
import { Icon } from "~/components/input/Icon";
import { LoaderCircle } from "~/components/input/LoaderCircle";
import { useCloseOnEscape } from "~/utils/hooks";
import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from 'react';
import styles from './styles.module.scss';
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
import { Icon } from '~/components/input/Icon';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { useCloseOnEscape } from '~/utils/hooks';
interface IProps {
children: React.ReactChild;
@ -14,7 +14,7 @@ interface IProps {
width?: number;
error?: string;
is_loading?: boolean;
overlay?: ReactElement;
overlay?: JSX.Element;
onOverlayClick?: MouseEventHandler<HTMLDivElement>;
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 { IDialogProps } from '~/redux/modal/constants';
import { useCloseOnEscape } from '~/utils/hooks';
@ -16,6 +24,7 @@ import { EMPTY_NODE, NODE_EDITORS } from '~/redux/node/constants';
import { BetterScrollDialog } from '../BetterScrollDialog';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { IEditorComponentProps } from '~/redux/node/types';
import { has, values } from 'ramda';
const mapStateToProps = state => {
const { editor, errors } = selectNode(state);
@ -32,7 +41,7 @@ const mapDispatchToProps = {
type IProps = IDialogProps &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {
type: keyof typeof NODE_EDITORS;
type: string;
};
const EditorDialogUnconnected: FC<IProps> = ({
@ -44,7 +53,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
type,
}) => {
const [data, setData] = useState(EMPTY_NODE);
const [temp, setTemp] = useState([]);
const [temp, setTemp] = useState<string[]>([]);
useEffect(() => setData(editor), [editor]);
@ -93,9 +102,18 @@ const EditorDialogUnconnected: FC<IProps> = ({
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 (
<form onSubmit={onSubmit} className={styles.form}>
@ -107,7 +125,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
onClose={onRequestClose}
>
<div className={styles.editor}>
{createElement(NODE_EDITORS[type], {
{createElement(component, {
data,
setData,
temp,

View file

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

View file

@ -3,9 +3,10 @@ import { Button } from '~/components/input/Button';
import { Grid } from '~/components/containers/Grid';
import { Group } from '~/components/containers/Group';
import styles from './styles.module.scss';
import { ISocialProvider } from '~/redux/auth/types';
interface IProps {
openOauthWindow: (provider: string) => MouseEventHandler;
openOauthWindow: (provider: ISocialProvider) => MouseEventHandler;
}
const LoginDialogButtons: FC<IProps> = ({ openOauthWindow }) => (

View file

@ -24,7 +24,7 @@ const ModalUnconnected: FC<IProps> = ({
}) => {
const onRequestClose = useCallback(() => {
modalSetShown(false);
modalSetDialog(null);
modalSetDialog('');
}, [modalSetShown, modalSetDialog]);
if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null;
@ -43,10 +43,7 @@ const ModalUnconnected: FC<IProps> = ({
);
};
const Modal = connect(
mapStateToProps,
mapDispatchToProps
)(ModalUnconnected);
const Modal = connect(mapStateToProps, mapDispatchToProps)(ModalUnconnected);
export { ModalUnconnected, Modal };

View file

@ -78,7 +78,9 @@ const PhotoSwipeUnconnected: FC<Props> = ({ photoswipe, modalSetShown }) => {
useEffect(() => {
window.location.hash = 'preview';
return () => (window.location.hash = '');
return () => {
window.location.hash = '';
};
}, []);
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 { connect } from 'react-redux';
import { BetterScrollDialog } from '../BetterScrollDialog';
@ -49,7 +49,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: null, is_succesfull: false });
authSetRestore({ error: '', is_succesfull: false });
}
}, [password, password_again]);
@ -69,7 +69,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
<Icon icon="check" size={64} />
<div>Пароль обновлен</div>
<div>Добро пожаловать домой, ~{user.username}!</div>
<div>Добро пожаловать домой, ~{user?.username}!</div>
<div />
@ -77,14 +77,16 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
Ура!
</Button>
</Group>
) : null,
) : (
undefined
),
[is_succesfull]
);
const not_ready = useMemo(() => (is_loading && !user ? <div className={styles.shade} /> : null), [
is_loading,
user,
]);
const not_ready = useMemo(
() => (is_loading && !user ? <div className={styles.shade} /> : undefined),
[is_loading, user]
);
const invalid_code = useMemo(
() =>
@ -100,7 +102,9 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
Очень жаль
</Button>
</Group>
) : null,
) : (
undefined
),
[is_loading, user, error]
);
@ -135,7 +139,7 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
type="password"
value={password_again}
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}>

View file

@ -43,7 +43,7 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: null, is_succesfull: false });
authSetRestore({ error: '', is_succesfull: false });
}
}, [field]);
@ -72,7 +72,9 @@ const RestoreRequestDialogUnconnected: FC<IProps> = ({
Отлично!
</Button>
</Group>
) : null,
) : (
undefined
),
[is_succesfull]
);

View file

@ -37,6 +37,7 @@ const BorisLayout: FC<IProps> = () => {
if (
user.last_seen_boris &&
last_comment.created_at &&
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
)
return;

View file

@ -12,9 +12,14 @@ import { NodeNoComments } from '~/components/node/NodeNoComments';
import { NodeRelated } from '~/components/node/NodeRelated';
import { NodeComments } from '~/components/node/NodeComments';
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 { pick } from 'ramda';
import { path, pick, prop } from 'ramda';
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
@ -71,9 +76,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
nodeStar,
nodeLock,
nodeSetCoverImage,
nodeLockComment,
nodeEditComment,
nodeLoadMoreComments,
modalShowPhotoswipe,
}) => {
const [layout, setLayout] = useState({});
@ -84,7 +86,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
comments = [],
current: node,
related,
comment_data,
comment_count,
} = useShallowSelect(selectNode);
const updateLayout = useCallback(() => setLayout({}), []);
@ -103,6 +104,10 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
const onTagClick = useCallback(
(tag: Partial<ITag>) => {
if (!node?.id || !tag?.title) {
return;
}
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
},
[history, node.id]
@ -112,9 +117,9 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
const can_star = useMemo(() => canStarNode(node, user), [node, user]);
const head = node && node.type && NODE_HEADS[node.type];
const block = node && node.type && NODE_COMPONENTS[node.type];
const inline = node && node.type && NODE_INLINES[node.type];
const head = useMemo(() => node?.type && prop(node?.type, NODE_HEADS), [node.type]);
const block = useMemo(() => node?.type && prop(node?.type, NODE_COMPONENTS), [node.type]);
const inline = useMemo(() => node?.type && prop(node?.type, NODE_INLINES), [node.type]);
const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]);
const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]);
@ -147,10 +152,10 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
return (
<>
{createNodeBlock(head)}
{!!head && createNodeBlock(head)}
<Card className={styles.node} seamless>
{createNodeBlock(block)}
{!!block && createNodeBlock(block)}
<NodePanel
node={pick(
@ -208,12 +213,13 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
{!is_loading &&
related &&
related.albums &&
!!node?.id &&
Object.keys(related.albums)
.filter(album => related.albums[album].length > 0)
.map(album => (
<NodeRelated
title={
<Link to={URLS.NODE_TAG_URL(node.id, encodeURIComponent(album))}>
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>
{album}
</Link>
}

View file

@ -1,43 +1,42 @@
import React, { FC, useCallback, useEffect, useState } from "react";
import styles from "./styles.module.scss";
import { connect } from "react-redux";
import { getURL } from "~/utils/dom";
import { pick } from "ramda";
import { selectAuthProfile, selectAuthUser } from "~/redux/auth/selectors";
import { PRESETS } from "~/constants/urls";
import { selectUploads } from "~/redux/uploads/selectors";
import { IFileWithUUID } from "~/redux/types";
import uuid from "uuid4";
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from "~/redux/uploads/constants";
import React, { FC, useCallback, useEffect, useState } from 'react';
import styles from './styles.module.scss';
import { connect } from 'react-redux';
import { getURL } from '~/utils/dom';
import { pick } from 'ramda';
import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors';
import { PRESETS } from '~/constants/urls';
import { selectUploads } from '~/redux/uploads/selectors';
import { IFileWithUUID } from '~/redux/types';
import uuid from 'uuid4';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
import { path } from 'ramda';
import * as UPLOAD_ACTIONS from "~/redux/uploads/actions";
import * as AUTH_ACTIONS from "~/redux/auth/actions";
import { Icon } from "~/components/input/Icon";
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { Icon } from '~/components/input/Icon';
const mapStateToProps = state => ({
user: pick(["id"], selectAuthUser(state)),
profile: pick(["is_loading", "user"], selectAuthProfile(state)),
uploads: pick(["statuses", "files"], selectUploads(state))
user: pick(['id'], selectAuthUser(state)),
profile: pick(['is_loading', 'user'], selectAuthProfile(state)),
uploads: pick(['statuses', 'files'], selectUploads(state)),
});
const mapDispatchToProps = {
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
authPatchUser: AUTH_ACTIONS.authPatchUser
authPatchUser: AUTH_ACTIONS.authPatchUser,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const ProfileAvatarUnconnected: FC<IProps> = ({
user: { id },
profile: { is_loading, user },
uploads: { statuses, files },
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(() => {
if (!can_edit) return;
@ -45,7 +44,7 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
Object.entries(statuses).forEach(([id, status]) => {
if (temp === id && !!status.uuid && files[status.uuid]) {
authPatchUser({ photo: files[status.uuid] });
setTemp(null);
setTemp('');
}
});
}, [statuses, files, temp, can_edit, authPatchUser]);
@ -58,11 +57,11 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
temp_id: uuid(),
subject: UPLOAD_SUBJECTS.AVATAR,
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, setTemp]
@ -81,13 +80,15 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
[onUpload, can_edit]
);
const backgroundImage = is_loading
? undefined
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`;
return (
<div
className={styles.avatar}
style={{
backgroundImage: is_loading
? null
: `url("${user && getURL(user.photo, PRESETS.avatar)}")`
backgroundImage,
}}
>
{can_edit && <input type="file" onInput={onInputChange} />}
@ -100,9 +101,6 @@ const ProfileAvatarUnconnected: FC<IProps> = ({
);
};
const ProfileAvatar = connect(
mapStateToProps,
mapDispatchToProps
)(ProfileAvatarUnconnected);
const ProfileAvatar = connect(mapStateToProps, mapDispatchToProps)(ProfileAvatarUnconnected);
export { ProfileAvatar };

View file

@ -1,5 +1,5 @@
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 { Group } from '~/components/containers/Group';
import { Placeholder } from '~/components/placeholders/Placeholder';
@ -14,7 +14,7 @@ interface IProps {
is_loading?: boolean;
is_own?: boolean;
setTab?: (tab: string) => void;
setTab?: (tab: IAuthState['profile']['tab']) => void;
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.name}>
{is_loading ? <Placeholder width="80%" /> : user.fullname || user.username}
{is_loading ? <Placeholder width="80%" /> : user?.fullname || user?.username}
</div>
<div className={styles.description}>
{is_loading ? <Placeholder /> : getPrettyDate(user.last_seen)}
{is_loading ? <Placeholder /> : getPrettyDate(user?.last_seen)}
</div>
</div>
</Group>
<ProfileTabs tab={tab} is_own={is_own} setTab={setTab} />
<ProfileTabs tab={tab} is_own={!!is_own} setTab={setTab} />
{content}
</div>

View file

@ -20,10 +20,10 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
const {
params: { username },
} = useRouteMatch<{ username: string }>();
const [user, setUser] = useState<IUser>(null);
const [user, setUser] = useState<IUser | undefined>(undefined);
useEffect(() => {
if (user) setUser(null);
if (user) setUser(undefined);
}, [username]);
useEffect(() => {

View file

@ -31,7 +31,7 @@ const ProfileMessagesUnconnected: FC<IProps> = ({
messagesRefreshMessages,
}) => {
const wasAtBottom = useRef(true);
const [wrap, setWrap] = useState<HTMLDivElement>(null);
const [wrap, setWrap] = useState<HTMLDivElement | undefined>(undefined);
const [editingMessageId, setEditingMessageId] = useState(0);
const onEditMessage = useCallback((id: number) => setEditingMessageId(id), [setEditingMessageId]);
@ -95,31 +95,33 @@ const ProfileMessagesUnconnected: FC<IProps> = ({
if (!messages.messages.length || profile.is_loading)
return <NodeNoComments is_loading={messages.is_loading_messages || profile.is_loading} />;
return (
messages.messages.length > 0 && (
<div className={styles.messages} ref={storeRef}>
{messages.messages
.filter(message => !!message.text)
.map((
message // TODO: show files / memo
) => (
<Message
message={message}
incoming={id !== message.from.id}
key={message.id}
onEdit={onEditMessage}
onDelete={onDeleteMessage}
isEditing={editingMessageId === message.id}
onCancelEdit={onCancelEdit}
onRestore={onRestoreMessage}
/>
))}
if (messages.messages.length <= 0) {
return null;
}
{!messages.is_loading_messages && messages.messages.length > 0 && (
<div className={styles.placeholder}>Когда-нибудь здесь будут еще сообщения</div>
)}
</div>
)
return (
<div className={styles.messages} ref={storeRef}>
{messages.messages
.filter(message => !!message.text)
.map((
message // TODO: show files / memo
) => (
<Message
message={message}
incoming={id !== message.from.id}
key={message.id}
onEdit={onEditMessage}
onDelete={onDeleteMessage}
isEditing={editingMessageId === message.id}
onCancelEdit={onCancelEdit}
onRestore={onRestoreMessage}
/>
))}
{!messages.is_loading_messages && messages.messages.length > 0 && (
<div className={styles.placeholder}>Когда-нибудь здесь будут еще сообщения</div>
)}
</div>
);
};

View file

@ -1,11 +1,14 @@
import React, { FC, useMemo } from 'react';
import styles from './styles.module.scss';
import { IAuthState } from '~/redux/auth/types';
import { getURL } from '~/utils/dom';
import { formatText, getURL } from '~/utils/dom';
import { PRESETS, URLS } from '~/constants/urls';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { Link } from 'react-router-dom';
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 {
profile: IAuthState['profile'];
@ -26,11 +29,11 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile }) => {
<div className={styles.region_wrap}>
<div className={styles.region}>
<div className={styles.name}>
{profile.is_loading ? <Placeholder /> : profile.user.fullname}
{profile.is_loading ? <Placeholder /> : profile?.user?.fullname}
</div>
<div className={styles.username}>
{profile.is_loading ? <Placeholder /> : `~${profile.user.username}`}
{profile.is_loading ? <Placeholder /> : `~${profile?.user?.username}`}
</div>
<div className={styles.menu}>
@ -53,7 +56,9 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile }) => {
</div>
{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>
);

View file

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

View file

@ -56,7 +56,7 @@ const ProfileSidebarUnconnected: FC<Props> = ({
</Switch>
<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} />
<Filler />
</div>

View file

@ -35,7 +35,10 @@ const TagSidebarUnconnected: FC<Props> = ({ nodes, tagLoadNodes, tagSetNodes })
useEffect(() => {
tagLoadNodes(tag);
return () => tagSetNodes({ list: [], count: 0 });
return () => {
tagSetNodes({ list: [], count: 0 });
};
}, [tag]);
const loadMore = useCallback(() => {