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

#58 made dialogs as routes

This commit is contained in:
Fedor Katurov 2021-03-29 14:11:39 +07:00
parent d9af895558
commit 4dc8bea040
21 changed files with 230 additions and 172 deletions

View file

@ -1,58 +1,44 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { nodeCreate } from '~/redux/node/actions'; import { Link } from 'react-router-dom';
import styles from './styles.module.scss';
import { NODE_TYPES } from '~/redux/node/constants';
import classNames from 'classnames'; import classNames from 'classnames';
import { useRouteMatch } from 'react-router';
import styles from './styles.module.scss';
interface Props { interface Props {
isLab?: boolean; isLab?: boolean;
} }
const SubmitBar: FC<Props> = ({ isLab }) => { const SubmitBar: FC<Props> = ({ isLab }) => {
const dispatch = useDispatch(); const { url } = useRouteMatch();
const [focused, setFocused] = useState(false);
const onOpenImageEditor = useCallback(() => dispatch(nodeCreate(NODE_TYPES.IMAGE, isLab)), [ const onFocus = useCallback(() => setFocused(true), [setFocused]);
dispatch, const onBlur = useCallback(() => setFocused(false), [setFocused]);
]);
const onOpenTextEditor = useCallback(() => dispatch(nodeCreate(NODE_TYPES.TEXT, isLab)), [
dispatch,
]);
const onOpenVideoEditor = useCallback(() => dispatch(nodeCreate(NODE_TYPES.VIDEO, isLab)), [
dispatch,
]);
const onOpenAudioEditor = useCallback(() => dispatch(nodeCreate(NODE_TYPES.AUDIO, isLab)), [
dispatch,
]);
return ( return (
<div className={classNames(styles.wrap, { [styles.lab]: isLab })}> <div className={classNames(styles.wrap, { [styles.lab]: isLab })}>
<div className={styles.panel}> <div className={classNames(styles.panel, { [styles.active]: focused })}>
<div onClick={onOpenImageEditor}> <Link to={`${url}/create/image`} className={styles.link}>
<Icon icon="image" /> <Icon icon="image" size={32} />
</Link>
<Link to={`${url}/create/text`} className={styles.link}>
<Icon icon="text" size={32} />
</Link>
<Link to={`${url}/create/video`} className={styles.link}>
<Icon icon="video" size={32} />
</Link>
<Link to={`${url}/create/audio`} className={styles.link}>
<Icon icon="audio" size={32} />
</Link>
</div> </div>
<div onClick={onOpenTextEditor}> <button className={styles.button} onFocus={onFocus} onBlur={onBlur} type="button">
<Icon icon="text" />
</div>
<div onClick={onOpenVideoEditor}>
<Icon icon="video" />
</div>
<div onClick={onOpenAudioEditor}>
<Icon icon="audio" />
</div>
</div>
<div className={styles.button}>
<Icon icon="plus" /> <Icon icon="plus" />
</div> </button>
</div> </div>
); );
}; };

View file

@ -5,13 +5,7 @@
bottom: 0; bottom: 0;
left: 50%; left: 50%;
transform: translate($content_width / 2 + $gap, 0); transform: translate($content_width / 2 + $gap, 0);
z-index: 4; z-index: 14;
&:hover {
.panel {
transform: translate(0, 0);
}
}
@media (max-width: $content_width + ($bar_height + $gap) * 2) { @media (max-width: $content_width + ($bar_height + $gap) * 2) {
left: 100%; left: 100%;
@ -31,6 +25,8 @@
cursor: pointer; cursor: pointer;
position: relative; position: relative;
z-index: 2; z-index: 2;
border: none;
outline: none;
svg { svg {
width: 32px; width: 32px;
@ -50,16 +46,25 @@
padding-bottom: $bar_height; padding-bottom: $bar_height;
border-radius: $radius $radius 0 0; border-radius: $radius $radius 0 0;
transform: translate(0, 100%); transform: translate(0, 100%);
transition: transform 250ms; transition: transform 250ms 250ms;
div { &.active {
transform: translate(0, 0);
transition: transform 250ms;
}
}
.link {
@include outer_shadow; @include outer_shadow;
height: $bar_height; height: $bar_height;
width: $bar_height; width: $bar_height;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
fill: white;
color: white;
svg { svg {
width: 32px; width: 32px;
@ -69,5 +74,4 @@
&:first-child { &:first-child {
border-radius: $radius $radius 0 0; border-radius: $radius $radius 0 0;
} }
}
} }

View file

@ -0,0 +1,19 @@
import React, { FC, MouseEventHandler } from 'react';
import ReactDOM from 'react-dom';
import styles from './styles.module.scss';
type IProps = {
onOverlayClick: MouseEventHandler;
};
const ModalWrapper: FC<IProps> = ({ children, onOverlayClick }) => {
return ReactDOM.createPortal(
<div className={styles.fixed}>
<div className={styles.overlay} onClick={onOverlayClick} />
<div className={styles.content}>{children}</div>
</div>,
document.body
);
};
export { ModalWrapper };

View file

@ -5,6 +5,8 @@ import { INode } from '~/redux/types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Placeholder } from '~/components/placeholders/Placeholder'; import { Placeholder } from '~/components/placeholders/Placeholder';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
import { URLS } from '~/constants/urls';
import { Link } from 'react-router-dom';
interface IProps { interface IProps {
node: Partial<INode>; node: Partial<INode>;
@ -24,7 +26,7 @@ interface IProps {
const NodePanelInner: FC<IProps> = memo( const NodePanelInner: FC<IProps> = memo(
({ ({
node: { title, user, is_liked, is_heroic, deleted_at, created_at, like_count }, node: { id, title, user, is_liked, is_heroic, deleted_at, created_at, like_count },
stack, stack,
canStar, canStar,
@ -78,9 +80,9 @@ const NodePanelInner: FC<IProps> = memo(
<Icon icon={deleted_at ? 'locked' : 'unlocked'} size={24} onClick={onLock} /> <Icon icon={deleted_at ? 'locked' : 'unlocked'} size={24} onClick={onLock} />
</div> </div>
<div> <Link to={URLS.NODE_EDIT_URL(id)}>
<Icon icon="edit" size={24} onClick={onEdit} /> <Icon icon="edit" size={24} onClick={onEdit} />
</div> </Link>
</div> </div>
</div> </div>
)} )}

View file

@ -1,8 +1,4 @@
import { NODE_TYPES } from '~/redux/node/constants'; import { NODE_TYPES } from '~/redux/node/constants';
import { EditorDialogImage } from '~/containers/editors/EditorDialogImage';
import { EditorDialogText } from '~/containers/editors/EditorDialogText';
import { EditorDialogVideo } from '~/containers/editors/EditorDialogVideo';
import { EditorDialogAudio } from '~/containers/editors/EditorDialogAudio';
import { LoginDialog } from '~/containers/dialogs/LoginDialog'; import { LoginDialog } from '~/containers/dialogs/LoginDialog';
import { LoadingDialog } from '~/containers/dialogs/LoadingDialog'; import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
import { TestDialog } from '~/containers/dialogs/TestDialog'; import { TestDialog } from '~/containers/dialogs/TestDialog';
@ -16,10 +12,6 @@ import { IDialogProps } from '~/redux/types';
import { FC } from 'react'; import { FC } from 'react';
export const DIALOG_CONTENT: Record<string, FC<IDialogProps>> = { export const DIALOG_CONTENT: Record<string, FC<IDialogProps>> = {
[DIALOGS.EDITOR_IMAGE]: EditorDialogImage,
[DIALOGS.EDITOR_TEXT]: EditorDialogText,
[DIALOGS.EDITOR_VIDEO]: EditorDialogVideo,
[DIALOGS.EDITOR_AUDIO]: EditorDialogAudio,
[DIALOGS.LOGIN]: LoginDialog, [DIALOGS.LOGIN]: LoginDialog,
[DIALOGS.LOGIN_SOCIAL_REGISTER]: LoginSocialRegisterDialog, [DIALOGS.LOGIN_SOCIAL_REGISTER]: LoginSocialRegisterDialog,
[DIALOGS.LOADING]: LoadingDialog, [DIALOGS.LOADING]: LoadingDialog,

View file

@ -16,6 +16,8 @@ export const URLS = {
BACKEND_DOWN: '/oopsie', BACKEND_DOWN: '/oopsie',
}, },
NODE_URL: (id: INode['id'] | string) => `/post${id}`, NODE_URL: (id: INode['id'] | string) => `/post${id}`,
NODE_EDIT_URL: (id: INode['id'] | string) => `/post${id}/edit`,
NODE_CREATE_URL: (type: string) => `/`,
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

@ -0,0 +1,38 @@
import React, { FC, useCallback, useMemo, useRef } from 'react';
import { EMPTY_NODE, NODE_TYPES } from '~/redux/node/constants';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { useHistory, useRouteMatch } from 'react-router';
import { values } from 'ramda';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
const EditorCreateDialog: FC = () => {
const history = useHistory();
const {
params: { type },
url,
} = useRouteMatch<{ type: string }>();
const backUrl = useMemo(() => {
return url.replace(/\/create\/(.*)$/, '');
}, [url]);
const goBack = useCallback(() => {
history.replace(backUrl);
}, [backUrl, history]);
const isExist = useMemo(() => values(NODE_TYPES).some(el => el === type), [type]);
const data = useRef({ ...EMPTY_NODE, type });
if (!type || !isExist) {
return null;
}
return (
<ModalWrapper onOverlayClick={goBack}>
<EditorDialog node={data.current} onRequestClose={goBack} />
</ModalWrapper>
);
};
export { EditorCreateDialog };

View file

@ -11,22 +11,20 @@ import { EditorButtons } from '~/components/editors/EditorButtons';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader'; import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants'; import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { FormikProvider } from 'formik'; import { FormikProvider } from 'formik';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { INode } from '~/redux/types';
import { selectNodeEditor } from '~/redux/node/selectors';
interface Props extends IDialogProps { interface Props extends IDialogProps {
type: string; node: INode;
} }
const EditorDialog: FC<Props> = ({ type, onRequestClose }) => { const EditorDialog: FC<Props> = ({ node, onRequestClose }) => {
const editor = useShallowSelect(selectNodeEditor);
const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, []); const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, []);
const formik = useNodeFormFormik({ ...editor, type }, uploader, onRequestClose); const formik = useNodeFormFormik(node, uploader, onRequestClose);
const { values, handleSubmit } = formik; const { values, handleSubmit } = formik;
useCloseOnEscape(onRequestClose); useCloseOnEscape(onRequestClose);
const component = useMemo(() => prop(type, NODE_EDITORS), [type]); const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
if (!component) { if (!component) {
return null; return null;

View file

@ -0,0 +1,57 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { EMPTY_NODE } from '~/redux/node/constants';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { useHistory, useRouteMatch } from 'react-router';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { apiGetNodeWithCancel } from '~/redux/node/api';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import styles from './styles.module.scss';
const EditorEditDialog: FC = () => {
const [data, setData] = useState(EMPTY_NODE);
const [isLoading, setLoading] = useState(false);
const history = useHistory();
const {
params: { id },
url,
} = useRouteMatch<{ id: string }>();
const backUrl = useMemo(() => {
return url.replace(/\/edit$/, '');
}, [url]);
const goBack = useCallback(() => {
history.replace(backUrl);
}, [backUrl, history]);
useEffect(() => {
if (!id) {
return;
}
const { request, cancel } = apiGetNodeWithCancel({ id });
setLoading(true);
request
.then(data => setData(data.node))
.then(() => setLoading(false))
.catch(console.log);
return () => cancel();
}, [id]);
return (
<ModalWrapper onOverlayClick={console.log}>
{isLoading ? (
<div className={styles.loader}>
<LoaderCircle size={64} />
</div>
) : (
<EditorDialog node={data} onRequestClose={goBack} />
)}
</ModalWrapper>
);
};
export { EditorEditDialog };

View file

@ -0,0 +1,4 @@
.loader {
fill: white;
color: white;
}

View file

@ -1,57 +1,34 @@
import React, { Attributes, FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import ReactDOM from 'react-dom';
import styles from './styles.module.scss';
import { IState } from '~/redux/store';
import * as ACTIONS from '~/redux/modal/actions';
import { DIALOG_CONTENT } from '~/constants/dialogs'; import { DIALOG_CONTENT } from '~/constants/dialogs';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectModal } from '~/redux/modal/selectors';
import { modalSetDialog, modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
const mapStateToProps = ({ modal }: IState) => ({ ...modal }); type IProps = {};
const mapDispatchToProps = {
modalSetShown: ACTIONS.modalSetShown,
modalSetDialog: ACTIONS.modalSetDialog,
modalShowDialog: ACTIONS.modalShowDialog,
};
type IProps = typeof mapDispatchToProps & ReturnType<typeof mapStateToProps> & {}; const Modal: FC<IProps> = ({}) => {
const { is_shown, dialog } = useShallowSelect(selectModal);
const dispatch = useDispatch();
const ModalUnconnected: FC<IProps> = ({
modalSetShown,
modalSetDialog,
modalShowDialog,
is_shown,
dialog,
}) => {
const onRequestClose = useCallback(() => { const onRequestClose = useCallback(() => {
modalSetShown(false); dispatch(modalSetShown(false));
modalSetDialog(''); dispatch(modalSetDialog(''));
}, [modalSetShown, modalSetDialog]); }, [dispatch]);
const onDialogChange = useCallback((val: string) => dispatch(modalShowDialog(val)), [dispatch]);
if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null; if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null;
return ReactDOM.createPortal( return (
<div className={styles.fixed}> <ModalWrapper onOverlayClick={onRequestClose}>
<div className={styles.overlay} onClick={onRequestClose} />
<div className={styles.content}>
{React.createElement(DIALOG_CONTENT[dialog], { {React.createElement(DIALOG_CONTENT[dialog], {
onRequestClose, onRequestClose,
onDialogChange: modalShowDialog, onDialogChange,
})} })}
</div> </ModalWrapper>
</div>,
document.body
); );
}; };
const Modal = connect(mapStateToProps, mapDispatchToProps)(ModalUnconnected); export { Modal };
export { ModalUnconnected, Modal };
/*
<div className={styles.content_scroller}>
<div className={styles.content_padder}>
</div>
</div>
*/

View file

@ -1,10 +0,0 @@
import React, { FC } from 'react';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { IDialogProps } from '~/redux/types';
import { NODE_TYPES } from '~/redux/node/constants';
type IProps = IDialogProps & {};
const EditorDialogAudio: FC<IProps> = props => <EditorDialog type={NODE_TYPES.AUDIO} {...props} />;
export { EditorDialogAudio };

View file

@ -1,10 +0,0 @@
import React, { FC } from 'react';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { IDialogProps } from '~/redux/types';
import { NODE_TYPES } from '~/redux/node/constants';
type IProps = IDialogProps & {};
const EditorDialogImage: FC<IProps> = props => <EditorDialog type={NODE_TYPES.IMAGE} {...props} />;
export { EditorDialogImage };

View file

@ -1,10 +0,0 @@
import React, { FC } from 'react';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { IDialogProps } from '~/redux/types';
import { NODE_TYPES } from '~/redux/node/constants';
type IProps = IDialogProps & {};
const EditorDialogText: FC<IProps> = props => <EditorDialog type={NODE_TYPES.TEXT} {...props} />;
export { EditorDialogText };

View file

@ -1,10 +0,0 @@
import React, { FC } from 'react';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { IDialogProps } from '~/redux/types';
import { NODE_TYPES } from '~/redux/node/constants';
type IProps = IDialogProps & {};
const EditorDialogVideo: FC<IProps> = props => <EditorDialog type={NODE_TYPES.VIDEO} {...props} />;
export { EditorDialogVideo };

View file

@ -5,6 +5,7 @@ import { TagSidebar } from '~/containers/sidebars/TagSidebar';
import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar'; import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar';
import { Authorized } from '~/components/containers/Authorized'; import { Authorized } from '~/components/containers/Authorized';
import { SubmitBar } from '~/components/bars/SubmitBar'; import { SubmitBar } from '~/components/bars/SubmitBar';
import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog';
interface IProps { interface IProps {
prefix?: string; prefix?: string;
@ -15,6 +16,7 @@ const SidebarRouter: FC<IProps> = ({ prefix = '', isLab }) => {
return createPortal( return createPortal(
<> <>
<Switch> <Switch>
<Route path={`${prefix}/create/:type`} component={EditorCreateDialog} />
<Route path={`${prefix}/tag/:tag`} component={TagSidebar} /> <Route path={`${prefix}/tag/:tag`} component={TagSidebar} />
<Route path={`${prefix}/~:username`} component={ProfileSidebar} /> <Route path={`${prefix}/~:username`} component={ProfileSidebar} />
</Switch> </Switch>

View file

@ -1,5 +1,5 @@
import React, { FC, memo } from 'react'; import React, { FC, memo } from 'react';
import { RouteComponentProps } from 'react-router'; import { Route, RouteComponentProps } from 'react-router';
import { selectNode } from '~/redux/node/selectors'; import { selectNode } from '~/redux/node/selectors';
import { Card } from '~/components/containers/Card'; import { Card } from '~/components/containers/Card';
@ -15,6 +15,8 @@ import { NodeBottomBlock } from '~/components/node/NodeBottomBlock';
import { useNodeCoverImage } from '~/utils/hooks/node/useNodeCoverImage'; import { useNodeCoverImage } from '~/utils/hooks/node/useNodeCoverImage';
import { useScrollToTop } from '~/utils/hooks/useScrollToTop'; import { useScrollToTop } from '~/utils/hooks/useScrollToTop';
import { useLoadNode } from '~/utils/hooks/node/useLoadNode'; import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
import { URLS } from '~/constants/urls';
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
type IProps = RouteComponentProps<{ id: string }> & {}; type IProps = RouteComponentProps<{ id: string }> & {};
@ -64,6 +66,8 @@ const NodeLayout: FC<IProps> = memo(
</Container> </Container>
<SidebarRouter prefix="/post:id" /> <SidebarRouter prefix="/post:id" />
<Route path={URLS.NODE_EDIT_URL(':id')} component={EditorEditDialog} />
</div> </div>
); );
} }

View file

@ -27,5 +27,5 @@ export const MODAL_ACTIONS = {
export interface IDialogProps { export interface IDialogProps {
onRequestClose: () => void; onRequestClose: () => void;
onDialogChange: (dialog: ValueOf<typeof DIALOGS>) => void; onDialogChange?: (dialog: ValueOf<typeof DIALOGS>) => void;
} }

View file

@ -1,5 +1,5 @@
import { api, cleanResult, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api'; import { api, cleanResult } from '~/utils/api';
import { IComment, INode, IResultWithStatus } from '../types'; import { IComment, INode } from '../types';
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { COMMENTS_DISPLAY } from './constants'; import { COMMENTS_DISPLAY } from './constants';
import { import {
@ -22,6 +22,7 @@ import {
GetNodeDiffRequest, GetNodeDiffRequest,
GetNodeDiffResult, GetNodeDiffResult,
} from '~/redux/node/types'; } from '~/redux/node/types';
import axios, { AxiosRequestConfig } from 'axios';
export type ApiPostNodeRequest = { node: INode }; export type ApiPostNodeRequest = { node: INode };
export type ApiPostNodeResult = { export type ApiPostNodeResult = {
@ -62,8 +63,20 @@ export const getNodeDiff = ({
}) })
.then(cleanResult); .then(cleanResult);
export const apiGetNode = ({ id }: ApiGetNodeRequest) => export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) =>
api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id)).then(cleanResult); api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id), config).then(cleanResult);
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
const cancelToken = axios.CancelToken.source();
return {
request: api
.get<ApiGetNodeResult>(API.NODE.GET_NODE(id), {
cancelToken: cancelToken.token,
})
.then(cleanResult),
cancel: cancelToken.cancel,
};
};
export const apiPostComment = ({ id, data }: ApiPostCommentRequest) => export const apiPostComment = ({ id, data }: ApiPostCommentRequest) =>
api.post<ApiPostCommentResult>(API.NODE.COMMENT(id), data).then(cleanResult); api.post<ApiPostCommentResult>(API.NODE.COMMENT(id), data).then(cleanResult);