mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
Merge branch 'master' into develop
This commit is contained in:
commit
ab1bfd600a
52 changed files with 1264 additions and 226 deletions
15
src/containers/main/SidebarRouter/index.tsx
Normal file
15
src/containers/main/SidebarRouter/index.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
import { TagSidebar } from '~/containers/sidebars/TagSidebar';
|
||||
|
||||
interface IProps {
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
const SidebarRouter: FC<IProps> = ({ prefix = '' }) => (
|
||||
<Switch>
|
||||
<Route path={`${prefix}/tag/:tag`} component={TagSidebar} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
export { SidebarRouter };
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import React, { createElement, FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { RouteComponentProps, useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
|
@ -12,12 +12,7 @@ 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 {
|
||||
NODE_COMPONENTS,
|
||||
NODE_INLINES,
|
||||
NODE_HEADS,
|
||||
INodeComponentProps,
|
||||
} 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/es/pick';
|
||||
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
||||
|
@ -25,12 +20,16 @@ import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
|||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||
import { Sticky } from '~/components/containers/Sticky';
|
||||
import { Footer } from '~/components/main/Footer';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import * as styles from './styles.scss';
|
||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectModal } from '~/redux/modal/selectors';
|
||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||
import { ITag } from '~/redux/types';
|
||||
import { URLS } from '~/constants/urls';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
node: selectNode(state),
|
||||
|
@ -86,6 +85,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
modalShowPhotoswipe,
|
||||
}) => {
|
||||
const [layout, setLayout] = useState({});
|
||||
const history = useHistory();
|
||||
|
||||
const updateLayout = useCallback(() => setLayout({}), []);
|
||||
|
||||
|
@ -101,6 +101,13 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
[node, nodeUpdateTags]
|
||||
);
|
||||
|
||||
const onTagClick = useCallback(
|
||||
(tag: Partial<ITag>) => {
|
||||
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
|
||||
},
|
||||
[history, node.id]
|
||||
);
|
||||
|
||||
const can_edit = useMemo(() => canEditNode(node, user), [node, user]);
|
||||
const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
|
||||
const can_star = useMemo(() => canStarNode(node, user), [node, user]);
|
||||
|
@ -197,6 +204,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
is_editable={is_user}
|
||||
tags={node.tags}
|
||||
onChange={onTagsChange}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -209,7 +217,11 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
.filter(album => related.albums[album].length > 0)
|
||||
.map(album => (
|
||||
<NodeRelated
|
||||
title={album}
|
||||
title={
|
||||
<Link to={URLS.NODE_TAG_URL(node.id, encodeURIComponent(album))}>
|
||||
{album}
|
||||
</Link>
|
||||
}
|
||||
items={related.albums[album]}
|
||||
key={album}
|
||||
/>
|
||||
|
@ -231,6 +243,8 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
|
||||
<Footer />
|
||||
</Card>
|
||||
|
||||
<SidebarRouter prefix="/post:id" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
justify-content: flex-start;
|
||||
padding-left: $gap / 2;
|
||||
min-width: 0;
|
||||
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding-left: 0;
|
||||
padding-top: $comment_height / 2;
|
||||
|
|
34
src/containers/sidebars/SidebarWrapper/index.tsx
Normal file
34
src/containers/sidebars/SidebarWrapper/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { FC, useEffect, useRef } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
|
||||
import { useCloseOnEscape } from '~/utils/hooks';
|
||||
|
||||
interface IProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const SidebarWrapper: FC<IProps> = ({ children, onClose }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useCloseOnEscape(onClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
|
||||
|
||||
return () => enableBodyScroll(ref.current);
|
||||
}, [ref.current]);
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.clicker} onClick={onClose} />
|
||||
<div className={styles.content} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export { SidebarWrapper };
|
55
src/containers/sidebars/SidebarWrapper/styles.module.scss
Normal file
55
src/containers/sidebars/SidebarWrapper/styles.module.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
@keyframes appear {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
z-index: 20;
|
||||
justify-content: flex-end;
|
||||
overflow: hidden;
|
||||
animation: appear 0.25s forwards;
|
||||
|
||||
@include can_backdrop {
|
||||
background: transparentize($content_bg, 0.15);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 1 33vw;
|
||||
width: 33vw;
|
||||
min-width: 480px;
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
animation: slideIn 0.5s 0.1s forwards;
|
||||
transform: translate(100%, 0);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.clicker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
102
src/containers/sidebars/TagSidebar/index.tsx
Normal file
102
src/containers/sidebars/TagSidebar/index.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
|
||||
import styles from './styles.module.scss';
|
||||
import { useHistory, useRouteMatch } from 'react-router';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TagSidebarList } from '~/components/sidebar/TagSidebarList';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectTagNodes } from '~/redux/tag/selectors';
|
||||
import * as ACTIONS from '~/redux/tag/actions';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
|
||||
import { Tag } from '~/components/tags/Tag';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
nodes: selectTagNodes(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
tagLoadNodes: ACTIONS.tagLoadNodes,
|
||||
tagSetNodes: ACTIONS.tagSetNodes,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const TagSidebarUnconnected: FC<Props> = ({ nodes, tagLoadNodes, tagSetNodes }) => {
|
||||
const {
|
||||
params: { tag },
|
||||
url,
|
||||
} = useRouteMatch<{ tag: string }>();
|
||||
const history = useHistory();
|
||||
|
||||
const basePath = url.replace(new RegExp(`\/tag\/${tag}$`), '');
|
||||
|
||||
useEffect(() => {
|
||||
tagLoadNodes(tag);
|
||||
return () => tagSetNodes({ list: [], count: 0 });
|
||||
}, [tag]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (nodes.isLoading) return;
|
||||
tagLoadNodes(tag);
|
||||
}, [tagLoadNodes, tag, nodes.isLoading]);
|
||||
|
||||
const title = useMemo(() => decodeURIComponent(tag), [tag]);
|
||||
const progress = nodes.count > 0 ? `${(nodes.list.length / nodes.count) * 100}%` : '0';
|
||||
|
||||
const onClose = useCallback(() => history.push(basePath), [basePath]);
|
||||
const hasMore = nodes.count > nodes.list.length;
|
||||
|
||||
return (
|
||||
<SidebarWrapper onClose={onClose}>
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.head}>
|
||||
{nodes.count > 0 && (
|
||||
<div className={styles.progress}>
|
||||
<div className={styles.bar} style={{ width: progress }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.tag}>
|
||||
<Tag tag={{ title }} size="big" />
|
||||
</div>
|
||||
|
||||
{nodes.isLoading && (
|
||||
<div className={styles.sync}>
|
||||
<LoaderCircle size={20} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.close}>
|
||||
<Link to={basePath}>
|
||||
<Icon icon="close" size={32} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!nodes.count && !nodes.isLoading ? (
|
||||
<div className={styles.none}>
|
||||
<Icon icon="sad" size={120} />
|
||||
<div>
|
||||
У этого тэга нет постов
|
||||
<br />
|
||||
<br />
|
||||
Такие дела
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<InfiniteScroll hasMore={hasMore} loadMore={loadMore} className={styles.list}>
|
||||
<TagSidebarList nodes={nodes.list} />
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const TagSidebar = connect(mapStateToProps, mapDispatchToProps)(TagSidebarUnconnected);
|
||||
|
||||
export { TagSidebar };
|
105
src/containers/sidebars/TagSidebar/styles.module.scss
Normal file
105
src/containers/sidebars/TagSidebar/styles.module.scss
Normal file
|
@ -0,0 +1,105 @@
|
|||
.wrap {
|
||||
@include outer_shadow;
|
||||
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 0 1 400px;
|
||||
max-width: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: $content_bg;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $gap;
|
||||
background: lighten($content_bg, 2%);
|
||||
}
|
||||
|
||||
.tag {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1 1 100%;
|
||||
padding: $gap;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sync {
|
||||
padding-left: $gap * 2;
|
||||
|
||||
svg {
|
||||
fill: transparentize(white, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: darken($content_bg, 2%);
|
||||
height: 2px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: lighten($content_bg, 20%);
|
||||
}
|
||||
|
||||
.none {
|
||||
padding: 40px $gap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: transparentize(white, 0.9);
|
||||
fill: currentColor;
|
||||
font: $font_18_semibold;
|
||||
stroke: none;
|
||||
line-height: 26px;
|
||||
flex: 1;
|
||||
|
||||
div {
|
||||
margin-top: 24px;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue