From d3473eab4c97b6748f24b6a800fc53b2d0c4df4e Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Sat, 6 Mar 2021 13:17:39 +0700 Subject: [PATCH] #35 refactored node layout --- src/components/node/NodeBottomBlock/index.tsx | 75 ++++++ .../node/NodeCommentsBlock/index.tsx | 28 ++ .../node/NodeImageSlideBlock/index.tsx | 33 +-- .../node/NodeImageSwiperBlock/index.tsx | 4 +- src/components/node/NodePanel/index.tsx | 100 ++----- src/components/node/NodePanelInner/index.tsx | 28 +- .../node/NodeRelatedBlock/index.tsx | 44 +++ src/components/node/NodeTagsBlock/index.tsx | 52 ++++ src/containers/node/NodeLayout/index.tsx | 252 +++--------------- src/redux/node/actions.ts | 2 +- src/redux/node/constants.ts | 9 +- src/redux/node/reducer.ts | 10 +- src/redux/node/types.ts | 5 + src/utils/hooks/node/useLoadNode.ts | 13 + src/utils/hooks/node/useNodeActions.ts | 19 ++ src/utils/hooks/node/useNodeBlocks.ts | 34 +++ src/utils/hooks/node/useNodeCoverImage.ts | 17 ++ src/utils/hooks/node/useNodePermissions.ts | 14 + src/utils/hooks/useScrollToTop.ts | 7 + src/utils/hooks/user/userUser.ts | 4 + 20 files changed, 406 insertions(+), 344 deletions(-) create mode 100644 src/components/node/NodeBottomBlock/index.tsx create mode 100644 src/components/node/NodeCommentsBlock/index.tsx create mode 100644 src/components/node/NodeRelatedBlock/index.tsx create mode 100644 src/components/node/NodeTagsBlock/index.tsx create mode 100644 src/utils/hooks/node/useLoadNode.ts create mode 100644 src/utils/hooks/node/useNodeActions.ts create mode 100644 src/utils/hooks/node/useNodeBlocks.ts create mode 100644 src/utils/hooks/node/useNodeCoverImage.ts create mode 100644 src/utils/hooks/node/useNodePermissions.ts create mode 100644 src/utils/hooks/useScrollToTop.ts create mode 100644 src/utils/hooks/user/userUser.ts diff --git a/src/components/node/NodeBottomBlock/index.tsx b/src/components/node/NodeBottomBlock/index.tsx new file mode 100644 index 00000000..73a114e6 --- /dev/null +++ b/src/components/node/NodeBottomBlock/index.tsx @@ -0,0 +1,75 @@ +import React, { FC } from 'react'; +import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; +import { Group } from '~/components/containers/Group'; +import { Padder } from '~/components/containers/Padder'; +import styles from '~/containers/node/NodeLayout/styles.module.scss'; +import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock'; +import { NodeCommentForm } from '~/components/node/NodeCommentForm'; +import { Sticky } from '~/components/containers/Sticky'; +import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock'; +import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; +import { IComment, INode } from '~/redux/types'; +import { useUser } from '~/utils/hooks/user/userUser'; +import { NodeTagsBlock } from '~/components/node/NodeTagsBlock'; +import { INodeRelated } from '~/redux/node/types'; + +interface IProps { + node: INode; + isLoading: boolean; + commentsOrder: 'ASC' | 'DESC'; + comments: IComment[]; + commentsCount: number; + isLoadingComments: boolean; + related: INodeRelated; +} + +const NodeBottomBlock: FC = ({ + node, + isLoading, + isLoadingComments, + comments, + commentsCount, + commentsOrder, + related, +}) => { + const { inline } = useNodeBlocks(node, isLoading); + const { is_user } = useUser(); + + if (node.deleted_at) { + return ; + } + + return ( + + + + + {inline &&
{inline}
} + + + + {is_user && !isLoading && } +
+ +
+ + + + + + +
+
+
+
+ ); +}; + +export { NodeBottomBlock }; diff --git a/src/components/node/NodeCommentsBlock/index.tsx b/src/components/node/NodeCommentsBlock/index.tsx new file mode 100644 index 00000000..f111dbff --- /dev/null +++ b/src/components/node/NodeCommentsBlock/index.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { NodeNoComments } from '~/components/node/NodeNoComments'; +import { NodeComments } from '~/components/node/NodeComments'; +import { IComment, INode } from '~/redux/types'; +import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; +import { useUser } from '~/utils/hooks/user/userUser'; + +interface IProps { + order: 'ASC' | 'DESC'; + node: INode; + comments: IComment[]; + count: number; + isLoading: boolean; + isLoadingComments: boolean; +} + +const NodeCommentsBlock: FC = ({ isLoading, isLoadingComments, node, comments, count }) => { + const user = useUser(); + const { inline } = useNodeBlocks(node, isLoading); + + return isLoading || isLoadingComments || (!comments.length && !inline) ? ( + + ) : ( + + ); +}; + +export { NodeCommentsBlock }; diff --git a/src/components/node/NodeImageSlideBlock/index.tsx b/src/components/node/NodeImageSlideBlock/index.tsx index 85c5cbbd..746dde08 100644 --- a/src/components/node/NodeImageSlideBlock/index.tsx +++ b/src/components/node/NodeImageSlideBlock/index.tsx @@ -8,21 +8,24 @@ import { PRESETS } from '~/constants/urls'; import { throttle } from 'throttle-debounce'; import { Icon } from '~/components/input/Icon'; import { useArrows } from '~/utils/hooks/keys'; +import { useDispatch } from 'react-redux'; +import { modalShowPhotoswipe } from '~/redux/modal/actions'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { selectModal } from '~/redux/modal/selectors'; -interface IProps extends INodeComponentProps {} +interface IProps extends INodeComponentProps { + updateLayout?: () => void; +} const getX = event => (event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length) ? (event.touches.length && event.touches[0].clientX) || event.changedTouches[0].clientX : event.clientX; -const NodeImageSlideBlock: FC = ({ - node, - is_loading, - is_modal_shown, - updateLayout, - modalShowPhotoswipe, -}) => { +const NodeImageSlideBlock: FC = ({ node, isLoading, updateLayout = () => {} }) => { + const dispatch = useDispatch(); + const { is_shown } = useShallowSelect(selectModal); + const [current, setCurrent] = useState(0); const [height, setHeight] = useState(window.innerHeight - 143); const [max_height, setMaxHeight] = useState(960); @@ -88,7 +91,7 @@ const NodeImageSlideBlock: FC = ({ const { width } = wrap.current.getBoundingClientRect(); const fallback = window.innerHeight - 143; - if (is_loading) { + if (isLoading) { setHeight(fallback); return () => clearTimeout(timeout); } @@ -118,7 +121,7 @@ const NodeImageSlideBlock: FC = ({ return () => { if (timeout) clearTimeout(timeout); }; - }, [is_dragging, wrap, offset, heights, max_height, images, is_loading, updateLayout]); + }, [is_dragging, wrap, offset, heights, max_height, images, isLoading, updateLayout]); const onDrag = useCallback( event => { @@ -162,8 +165,8 @@ const NodeImageSlideBlock: FC = ({ normalizeOffset(); }, [wrap, setMaxHeight, normalizeOffset]); - const onOpenPhotoSwipe = useCallback(() => modalShowPhotoswipe(images, current), [ - modalShowPhotoswipe, + const onOpenPhotoSwipe = useCallback(() => dispatch(modalShowPhotoswipe(images, current)), [ + dispatch, images, current, ]); @@ -241,7 +244,7 @@ const NodeImageSlideBlock: FC = ({ images, ]); - useArrows(onNext, onPrev, is_modal_shown); + useArrows(onNext, onPrev, is_shown); useEffect(() => { setOffset(0); @@ -249,7 +252,7 @@ const NodeImageSlideBlock: FC = ({ return (
-
+
= ({ onTouchStart={startDragging} ref={slide} > - {!is_loading && + {!isLoading && images.map((file, index) => (
= () => ( -
SWIPER
-) +const NodeImageSwiperBlock: FC = () =>
SWIPER
; export { NodeImageSwiperBlock }; diff --git a/src/components/node/NodePanel/index.tsx b/src/components/node/NodePanel/index.tsx index 81530828..ab83d01c 100644 --- a/src/components/node/NodePanel/index.tsx +++ b/src/components/node/NodePanel/index.tsx @@ -1,85 +1,35 @@ -import React, { FC, useCallback, useEffect, useRef, useState, memo } from 'react'; +import React, { FC, memo } from 'react'; import styles from './styles.module.scss'; import { INode } from '~/redux/types'; -import { createPortal } from 'react-dom'; import { NodePanelInner } from '~/components/node/NodePanelInner'; +import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions'; +import { useNodeActions } from '~/utils/hooks/node/useNodeActions'; +import { shallowEqual } from 'react-redux'; interface IProps { - node: Partial; - layout: {}; - - can_edit: boolean; - can_like: boolean; - can_star: boolean; - - is_loading?: boolean; - - onEdit: () => void; - onLike: () => void; - onStar: () => void; - onLock: () => void; + node: INode; + isLoading: boolean; } -const NodePanel: FC = memo( - ({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => { - const [stack, setStack] = useState(false); +const NodePanel: FC = memo(({ node, isLoading }) => { + const [can_edit, can_like, can_star] = useNodePermissions(node); + const { onEdit, onLike, onStar, onLock } = useNodeActions(node); - const ref = useRef(null); - const getPlace = useCallback(() => { - if (!ref.current) return; - - const { bottom } = ref.current!.getBoundingClientRect(); - - setStack(bottom > window.innerHeight); - }, [ref]); - - useEffect(() => getPlace(), [layout]); - - useEffect(() => { - window.addEventListener('scroll', getPlace); - window.addEventListener('resize', getPlace); - - return () => { - window.removeEventListener('scroll', getPlace); - window.removeEventListener('resize', getPlace); - }; - }, [layout, getPlace]); - - return ( -
- {/* - stack && - createPortal( - , - document.body - ) - */} - - -
- ); - } -); + return ( +
+ +
+ ); +}, shallowEqual); export { NodePanel }; diff --git a/src/components/node/NodePanelInner/index.tsx b/src/components/node/NodePanelInner/index.tsx index 41e66d05..0a10c1b2 100644 --- a/src/components/node/NodePanelInner/index.tsx +++ b/src/components/node/NodePanelInner/index.tsx @@ -1,7 +1,5 @@ import React, { FC, memo } from 'react'; import styles from './styles.module.scss'; -import { Group } from '~/components/containers/Group'; -import { Filler } from '~/components/containers/Filler'; import { Icon } from '~/components/input/Icon'; import { INode } from '~/redux/types'; import classNames from 'classnames'; @@ -12,11 +10,11 @@ interface IProps { node: Partial; stack?: boolean; - can_edit: boolean; - can_like: boolean; - can_star: boolean; + canEdit: boolean; + canLike: boolean; + canStar: boolean; - is_loading: boolean; + isLoading: boolean; onEdit: () => void; onLike: () => void; @@ -29,11 +27,11 @@ const NodePanelInner: FC = memo( node: { title, user, is_liked, is_heroic, deleted_at, created_at, like_count }, stack, - can_star, - can_edit, - can_like, + canStar, + canEdit, + canLike, - is_loading, + isLoading, onStar, onEdit, @@ -45,12 +43,12 @@ const NodePanelInner: FC = memo(
- {is_loading ? : title || '...'} + {isLoading ? : title || '...'}
{user && user.username && (
- {is_loading ? ( + {isLoading ? ( ) : ( `~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}` @@ -59,14 +57,14 @@ const NodePanelInner: FC = memo( )}
- {can_edit && ( + {canEdit && (
- {can_star && ( + {canStar && (
{is_heroic ? ( @@ -88,7 +86,7 @@ const NodePanelInner: FC = memo( )}
- {can_like && ( + {canLike && (
{is_liked ? ( diff --git a/src/components/node/NodeRelatedBlock/index.tsx b/src/components/node/NodeRelatedBlock/index.tsx new file mode 100644 index 00000000..c4948cb7 --- /dev/null +++ b/src/components/node/NodeRelatedBlock/index.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; +import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; +import { NodeRelated } from '~/components/node/NodeRelated'; +import { URLS } from '~/constants/urls'; +import { INode } from '~/redux/types'; +import { INodeRelated } from '~/redux/node/types'; +import { Link } from 'react-router-dom'; + +interface IProps { + isLoading: boolean; + node: INode; + related: INodeRelated; +} + +const NodeRelatedBlock: FC = ({ isLoading, node, related }) => { + if (isLoading) { + return ; + } + + return ( +
+ {related && + related.albums && + !!node?.id && + Object.keys(related.albums) + .filter(album => related.albums[album].length > 0) + .map(album => ( + {album} + } + items={related.albums[album]} + key={album} + /> + ))} + + {related && related.similar && related.similar.length > 0 && ( + + )} +
+ ); +}; + +export { NodeRelatedBlock }; diff --git a/src/components/node/NodeTagsBlock/index.tsx b/src/components/node/NodeTagsBlock/index.tsx new file mode 100644 index 00000000..e4ee877f --- /dev/null +++ b/src/components/node/NodeTagsBlock/index.tsx @@ -0,0 +1,52 @@ +import React, { FC, useCallback } from 'react'; +import { INode, ITag } from '~/redux/types'; +import { URLS } from '~/constants/urls'; +import { nodeUpdateTags } from '~/redux/node/actions'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; +import { NodeTags } from '~/components/node/NodeTags'; +import { useUser } from '~/utils/hooks/user/userUser'; + +interface IProps { + node: INode; + isLoading: boolean; +} + +const NodeTagsBlock: FC = ({ node, isLoading }) => { + const dispatch = useDispatch(); + const history = useHistory(); + const { is_user } = useUser(); + + const onTagsChange = useCallback( + (tags: string[]) => { + dispatch(nodeUpdateTags(node.id, tags)); + }, + [dispatch, node] + ); + + const onTagClick = useCallback( + (tag: Partial) => { + if (!node?.id || !tag?.title) { + return; + } + + history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title))); + }, + [history, node] + ); + + if (isLoading) { + return null; + } + + return ( + + ); +}; + +export { NodeTagsBlock }; diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index 45e4b59d..589b9072 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -1,254 +1,64 @@ -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 React, { FC, memo } from 'react'; +import { RouteComponentProps } from 'react-router'; import { selectNode } from '~/redux/node/selectors'; import { Card } from '~/components/containers/Card'; import { NodePanel } from '~/components/node/NodePanel'; -import { Group } from '~/components/containers/Group'; -import { Padder } from '~/components/containers/Padder'; -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 { selectUser } from '~/redux/auth/selectors'; -import { pick, prop } from 'ramda'; -import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; -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 styles from './styles.module.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'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { Container } from '~/containers/main/Container'; +import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; +import { NodeBottomBlock } from '~/components/node/NodeBottomBlock'; +import { useNodeCoverImage } from '~/utils/hooks/node/useNodeCoverImage'; +import { useScrollToTop } from '~/utils/hooks/useScrollToTop'; +import { useLoadNode } from '~/utils/hooks/node/useLoadNode'; -const mapStateToProps = (state: IState) => ({ - node: selectNode(state), - user: selectUser(state), - modal: pick(['is_shown'])(selectModal(state)), -}); +type IProps = RouteComponentProps<{ id: string }> & {}; -const mapDispatchToProps = { - nodeGotoNode: NODE_ACTIONS.nodeGotoNode, - nodeUpdateTags: NODE_ACTIONS.nodeUpdateTags, - nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage, - nodeEdit: NODE_ACTIONS.nodeEdit, - nodeLike: NODE_ACTIONS.nodeLike, - nodeStar: NODE_ACTIONS.nodeStar, - nodeLock: NODE_ACTIONS.nodeLock, - nodeLockComment: NODE_ACTIONS.nodeLockComment, - nodeEditComment: NODE_ACTIONS.nodeEditComment, - nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments, - modalShowPhotoswipe: MODAL_ACTIONS.modalShowPhotoswipe, -}; - -type IProps = ReturnType & - typeof mapDispatchToProps & - RouteComponentProps<{ id: string }> & {}; - -const NodeLayoutUnconnected: FC = memo( +const NodeLayout: FC = memo( ({ match: { params: { id }, }, - modal: { is_shown: is_modal_shown }, - user, - user: { is_user }, - nodeGotoNode, - nodeUpdateTags, - nodeEdit, - nodeLike, - nodeStar, - nodeLock, - nodeSetCoverImage, - modalShowPhotoswipe, }) => { - const [layout, setLayout] = useState({}); - const history = useHistory(); const { is_loading, - is_loading_comments, - comments = [], - current: node, - related, + current, + comments, comment_count, + is_loading_comments, + related, } = useShallowSelect(selectNode); - const updateLayout = useCallback(() => setLayout({}), []); - useEffect(() => { - if (is_loading) return; - nodeGotoNode(parseInt(id, 10), null); - }, [nodeGotoNode, id]); + useNodeCoverImage(current); + useScrollToTop([id]); + useLoadNode(id, is_loading); - const onTagsChange = useCallback( - (tags: string[]) => { - nodeUpdateTags(node.id, tags); - }, - [node, nodeUpdateTags] - ); - - const onTagClick = useCallback( - (tag: Partial) => { - if (!node?.id || !tag?.title) { - return; - } - - 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]); - - 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]); - const onStar = useCallback(() => nodeStar(node.id), [nodeStar, node]); - const onLock = useCallback(() => nodeLock(node.id, !node.deleted_at), [nodeStar, node]); - - const createNodeBlock = useCallback( - (block: FC) => - block && - createElement(block, { - node, - is_loading, - updateLayout, - layout, - modalShowPhotoswipe, - is_modal_shown, - }), - [node, is_loading, updateLayout, layout, modalShowPhotoswipe, is_modal_shown] - ); - - useEffect(() => { - if (!node.cover) return; - nodeSetCoverImage(node.cover); - return () => nodeSetCoverImage(null); - }, [nodeSetCoverImage, node.cover]); - - useEffect(() => { - window.scrollTo(0, 0); - }, [id]); + const { head, block } = useNodeBlocks(current, is_loading); return ( <> - {!!head && createNodeBlock(head)} + {head} - {!!block && createNodeBlock(block)} + {block} - + + - {node.deleted_at ? ( - - ) : ( - - - - - {inline &&
{createNodeBlock(inline)}
} - - {is_loading || is_loading_comments || (!comments.length && !inline) ? ( - - ) : ( - - )} - - {is_user && !is_loading && } -
- -
- - - {!is_loading && ( - - )} - - {is_loading && } - - {!is_loading && - related && - related.albums && - !!node?.id && - Object.keys(related.albums) - .filter(album => related.albums[album].length > 0) - .map(album => ( - - {album} - - } - items={related.albums[album]} - key={album} - /> - ))} - - {!is_loading && - related && - related.similar && - related.similar.length > 0 && ( - - )} - - -
-
-
-
- )} -