diff --git a/package.json b/package.json index 4374ead7..ddd058ae 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "redux-saga": "^1.1.1", "resize-sensor": "^0.0.6", "sticky-sidebar": "^3.3.1", + "swiper": "^6.5.0", "throttle-debounce": "^2.1.0", "typescript": "^4.0.5", "uuid4": "^1.1.4", @@ -71,6 +72,7 @@ "@types/ramda": "^0.26.33", "@types/react-redux": "^7.1.11", "@types/yup": "^0.29.11", + "@types/swiper": "^5.4.2", "craco-alias": "^2.1.1", "craco-fast-refresh": "^1.0.2", "prettier": "^1.18.2" diff --git a/src/components/containers/Sticky/index.tsx b/src/components/containers/Sticky/index.tsx index 79d57d3c..f3e817d5 100644 --- a/src/components/containers/Sticky/index.tsx +++ b/src/components/containers/Sticky/index.tsx @@ -11,22 +11,22 @@ interface IProps extends DetailsHTMLAttributes {} const Sticky: FC = ({ children }) => { const ref = useRef(null); - let sb; + const sb = useRef(null); useEffect(() => { if (!ref.current) return; - sb = new StickySidebar(ref.current, { + sb.current = new StickySidebar(ref.current, { resizeSensor: true, topSpacing: 72, bottomSpacing: 10, }); - return () => sb.destroy(); - }, [ref.current, children]); + return () => sb.current?.destroy(); + }, [ref.current, sb.current, children]); if (sb) { - sb.updateSticky(); + sb.current?.updateSticky(); } return ( diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx index 94acddc4..2ac1119a 100644 --- a/src/components/editors/AudioEditor/index.tsx +++ b/src/components/editors/AudioEditor/index.tsx @@ -8,6 +8,8 @@ import { selectUploads } from '~/redux/uploads/selectors'; import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; import styles from './styles.module.scss'; import { NodeEditorProps } from '~/redux/node/types'; +import { useNodeImages } from '~/utils/hooks/node/useNodeImages'; +import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios'; const mapStateToProps = selectUploads; const mapDispatchToProps = { @@ -17,10 +19,7 @@ const mapDispatchToProps = { type IProps = ReturnType & typeof mapDispatchToProps & NodeEditorProps; const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) => { - const images = useMemo( - () => data.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), - [data.files] - ); + const images = useNodeImages(data); const pending_images = useMemo( () => @@ -30,10 +29,7 @@ const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) = [temp, statuses] ); - const audios = useMemo( - () => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), - [data.files] - ); + const audios = useNodeAudios(data); const pending_audios = useMemo( () => 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) => (
= ({ node }) => { + const dispatch = useDispatch(); + const [controlledSwiper, setControlledSwiper] = useState(undefined); + + const images = useNodeImages(node); + + const updateSwiper = useCallback(() => { + if (!controlledSwiper) return; + + controlledSwiper.updateSlides(); + controlledSwiper.updateSize(); + controlledSwiper.update(); + }, [controlledSwiper]); + + const resetSwiper = useCallback(() => { + if (!controlledSwiper) return; + controlledSwiper.slideTo(0, 0); + }, [controlledSwiper]); + + useEffect(() => { + updateSwiper(); + resetSwiper(); + }, [images, updateSwiper, resetSwiper]); + + const onOpenPhotoSwipe = useCallback( + () => dispatch(modalShowPhotoswipe(images, controlledSwiper?.activeIndex || 0)), + [dispatch, images, controlledSwiper] + ); + + if (!images?.length) { + return null; + } + + return ( +
+ + {images.map(file => ( + + {node.title} + + ))} + +
+ ); +}; + +export { NodeImageSwiperBlock }; diff --git a/src/components/node/NodeImageSwiperBlock/styles.module.scss b/src/components/node/NodeImageSwiperBlock/styles.module.scss new file mode 100644 index 00000000..22c422b3 --- /dev/null +++ b/src/components/node/NodeImageSwiperBlock/styles.module.scss @@ -0,0 +1,73 @@ +@import "~/styles/variables.scss"; + +.wrapper { + border-radius: $radius; + display: flex; + align-items: center; + justify-content: center; + + :global(.swiper-pagination) { + left: 50%; + bottom: $gap * 2; + transform: translate(-50%, 0); + background: darken($comment_bg, 4%); + width: auto; + padding: 5px 10px; + border-radius: 10px; + font: $font_10_semibold; + } + + :global(.swiper-container) { + width: 100vw; + } +} + +.slide { + text-align: center; + text-transform: uppercase; + font: $font_32_bold; + display: flex; + border-radius: $radius; + align-items: center; + justify-content: center; + width: auto; + max-width: 100vw; + opacity: 1; + transform: translate(0, 10px); + filter: brightness(50%) saturate(0.5); + transition: opacity 0.5s, filter 0.5s, transform 0.5s; + padding-bottom: $gap * 1.5; + padding-top: $gap; + + &:global(.swiper-slide-active) { + opacity: 1; + filter: brightness(100%); + transform: translate(0, 0); + } + + @include tablet { + padding-bottom: 0; + transform: translate(0, 0); + } +} + +.image { + max-height: calc(100vh - 70px - 70px); + max-width: 100%; + border-radius: $radius; + transition: box-shadow 1s; + box-shadow: transparentize(black, 0.7) 0 3px 5px; + + :global(.swiper-slide-active) & { + box-shadow: transparentize(black, 0.9) 0 10px 5px 4px, + transparentize(black, 0.7) 0 5px 5px, + transparentize(white, 0.95) 0 -1px 2px, + transparentize(white, 0.95) 0 -1px; + } + + @include tablet { + padding-bottom: 0; + max-height: 100vh; + border-radius: 0; + } +} 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/flow/FlowLayout/index.tsx b/src/containers/flow/FlowLayout/index.tsx index 179781b6..cf263080 100644 --- a/src/containers/flow/FlowLayout/index.tsx +++ b/src/containers/flow/FlowLayout/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useEffect, useCallback } from 'react'; +import React, { FC, useCallback, useEffect } from 'react'; import { connect } from 'react-redux'; import { FlowGrid } from '~/components/flow/FlowGrid'; import { selectFlow } from '~/redux/flow/selectors'; @@ -10,6 +10,7 @@ import { FlowHero } from '~/components/flow/FlowHero'; import styles from './styles.module.scss'; import { IState } from '~/redux/store'; import { FlowStamp } from '~/components/flow/FlowStamp'; +import { Container } from '~/containers/main/Container'; const mapStateToProps = (state: IState) => ({ flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading', 'search'], selectFlow(state)), @@ -61,28 +62,30 @@ const FlowLayoutUnconnected: FC = ({ }, []); return ( -
-
- -
+ +
+
+ +
-
- + +
+ +
- - -
+ ); }; diff --git a/src/containers/main/Container/index.tsx b/src/containers/main/Container/index.tsx new file mode 100644 index 00000000..d5045fd1 --- /dev/null +++ b/src/containers/main/Container/index.tsx @@ -0,0 +1,13 @@ +import React, { FC } from 'react'; +import styles from './styles.module.scss'; +import classNames from 'classnames'; + +interface IProps { + className?: string; +} + +const Container: FC = ({ className, children }) => ( +
{children}
+); + +export { Container }; diff --git a/src/containers/main/Container/styles.module.scss b/src/containers/main/Container/styles.module.scss new file mode 100644 index 00000000..cbf85b78 --- /dev/null +++ b/src/containers/main/Container/styles.module.scss @@ -0,0 +1,12 @@ +@import "~/styles/variables.scss"; + +.container { + width: 100%; + max-width: $content_width; + margin: auto; + padding: 0 $gap; + + @include tablet { + padding: 0; + } +} diff --git a/src/containers/main/MainLayout/styles.module.scss b/src/containers/main/MainLayout/styles.module.scss index e87b6e31..79ec32f8 100644 --- a/src/containers/main/MainLayout/styles.module.scss +++ b/src/containers/main/MainLayout/styles.module.scss @@ -2,24 +2,18 @@ .wrapper { width: 100%; - padding: 0 $gap; display: flex; flex-direction: column; box-sizing: border-box; align-items: center; justify-content: flex-start; flex: 1; - - @include tablet { - padding: 0; - } } .content { flex: 1; position: relative; width: 100%; - max-width: $content_width; display: flex; padding-bottom: 29px; flex-direction: column; diff --git a/src/containers/node/BorisLayout/index.tsx b/src/containers/node/BorisLayout/index.tsx index 49ef20d2..57127226 100644 --- a/src/containers/node/BorisLayout/index.tsx +++ b/src/containers/node/BorisLayout/index.tsx @@ -19,6 +19,7 @@ import { selectBorisStats } from '~/redux/boris/selectors'; import { authSetUser } from '~/redux/auth/actions'; import { nodeLoadNode } from '~/redux/node/actions'; import { borisLoadStats } from '~/redux/boris/actions'; +import { Container } from '~/containers/main/Container'; type IProps = {}; @@ -55,59 +56,61 @@ const BorisLayout: FC = () => { }, [dispatch]); return ( -
-
+ +
+
-
-
-
{title}
+
+
+
{title}
+
+ + Борис
- Борис -
+
+ + + {user.is_user && } -
- - - {user.is_user && } - - {node.is_loading_comments ? ( - - ) : ( - - )} - - -
- - - - - -
-

Господи-боженьки, где это я?

- -

- Всё впорядке, это — главный штаб Суицидальных Роботов, строителей Убежища. -

-

Здесь мы сидим и слушаем всё, что вас беспокоит.

-

Все виновные будут наказаны. Невиновные, впрочем, тоже.

-

// Такова жизнь.

-
- -
- -
+ {node.is_loading_comments ? ( + + ) : ( + + )}
-
-
+ +
+ + + + + +
+

Господи-боженьки, где это я?

+ +

+ Всё впорядке, это — главный штаб Суицидальных Роботов, строителей Убежища. +

+

Здесь мы сидим и слушаем всё, что вас беспокоит.

+

Все виновные будут наказаны. Невиновные, впрочем, тоже.

+

// Такова жизнь.

+
+ +
+ +
+
+
+
+
-
+ ); }; diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index 00c3ef7c..589b9072 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -1,249 +1,67 @@ -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 { path, 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 && ( - - )} - - -
-
-
-
- )} - -