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

made tags panel

This commit is contained in:
Fedor Katurov 2020-10-31 15:06:08 +07:00
parent f2289f4530
commit c4f60f3d81
31 changed files with 552 additions and 75 deletions

View file

@ -8,7 +8,7 @@ interface IProps {
const SidebarRouter: FC<IProps> = ({ prefix = '' }) => (
<Switch>
<Route path={`${prefix}/tag/:name`} component={TagSidebar} />
<Route path={`${prefix}/tag/:tag`} component={TagSidebar} />
</Switch>
);

View file

@ -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';
@ -32,6 +27,8 @@ 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),
@ -87,6 +84,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
modalShowPhotoswipe,
}) => {
const [layout, setLayout] = useState({});
const history = useHistory();
const updateLayout = useCallback(() => setLayout({}), []);
@ -102,6 +100,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]);
@ -198,6 +203,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
is_editable={is_user}
tags={node.tags}
onChange={onTagsChange}
onTagClick={onTagClick}
/>
)}

View file

@ -2,21 +2,27 @@ 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 {}
interface IProps {
onClose?: () => void;
}
const SidebarWrapper: FC<IProps> = ({ children }) => {
const SidebarWrapper: FC<IProps> = ({ children, onClose }) => {
const ref = useRef<HTMLDivElement>(null);
useCloseOnEscape(onClose);
useEffect(() => {
if (!ref.current) return;
disableBodyScroll(ref.current);
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>

View file

@ -1,3 +1,12 @@
@keyframes appear {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes slideIn {
100% { transform: translate(0, 0); }
}
.wrapper {
position: fixed;
top: 0;
@ -9,6 +18,8 @@
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);
@ -18,9 +29,27 @@
}
.content {
flex: 0 0 33vw;
flex: 0 1 33vw;
width: 33vw;
min-width: 480px;
max-width: 100vw;
height: 100%;
overflow: auto;
background: $content_bg;
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;
}

View file

@ -1,8 +1,102 @@
import React, { FC } from 'react';
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 { Tag } from '~/components/node/Tag';
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';
interface IProps {}
const mapStateToProps = state => ({
nodes: selectTagNodes(state),
});
const TagSidebar: FC<IProps> = () => <SidebarWrapper>TAGS</SidebarWrapper>;
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 };

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