mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
#35 refactored node layout
This commit is contained in:
parent
428c7e7a06
commit
d3473eab4c
20 changed files with 406 additions and 344 deletions
75
src/components/node/NodeBottomBlock/index.tsx
Normal file
75
src/components/node/NodeBottomBlock/index.tsx
Normal file
|
@ -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<IProps> = ({
|
||||
node,
|
||||
isLoading,
|
||||
isLoadingComments,
|
||||
comments,
|
||||
commentsCount,
|
||||
commentsOrder,
|
||||
related,
|
||||
}) => {
|
||||
const { inline } = useNodeBlocks(node, isLoading);
|
||||
const { is_user } = useUser();
|
||||
|
||||
if (node.deleted_at) {
|
||||
return <NodeDeletedBadge />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Padder>
|
||||
<Group horizontal className={styles.content}>
|
||||
<Group className={styles.comments}>
|
||||
{inline && <div className={styles.inline}>{inline}</div>}
|
||||
|
||||
<NodeCommentsBlock
|
||||
isLoading={isLoading}
|
||||
isLoadingComments={isLoadingComments}
|
||||
comments={comments}
|
||||
count={commentsCount}
|
||||
order={commentsOrder}
|
||||
node={node}
|
||||
/>
|
||||
|
||||
{is_user && !isLoading && <NodeCommentForm nodeId={node.id} />}
|
||||
</Group>
|
||||
|
||||
<div className={styles.panel}>
|
||||
<Sticky>
|
||||
<Group style={{ flex: 1, minWidth: 0 }}>
|
||||
<NodeTagsBlock node={node} isLoading={isLoading} />
|
||||
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />
|
||||
</Group>
|
||||
</Sticky>
|
||||
</div>
|
||||
</Group>
|
||||
</Padder>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export { NodeBottomBlock };
|
28
src/components/node/NodeCommentsBlock/index.tsx
Normal file
28
src/components/node/NodeCommentsBlock/index.tsx
Normal file
|
@ -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<IProps> = ({ isLoading, isLoadingComments, node, comments, count }) => {
|
||||
const user = useUser();
|
||||
const { inline } = useNodeBlocks(node, isLoading);
|
||||
|
||||
return isLoading || isLoadingComments || (!comments.length && !inline) ? (
|
||||
<NodeNoComments is_loading={isLoadingComments || isLoading} />
|
||||
) : (
|
||||
<NodeComments count={count} comments={comments} user={user} order="DESC" />
|
||||
);
|
||||
};
|
||||
|
||||
export { NodeCommentsBlock };
|
|
@ -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<IProps> = ({
|
||||
node,
|
||||
is_loading,
|
||||
is_modal_shown,
|
||||
updateLayout,
|
||||
modalShowPhotoswipe,
|
||||
}) => {
|
||||
const NodeImageSlideBlock: FC<IProps> = ({ 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<IProps> = ({
|
|||
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<IProps> = ({
|
|||
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<IProps> = ({
|
|||
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<IProps> = ({
|
|||
images,
|
||||
]);
|
||||
|
||||
useArrows(onNext, onPrev, is_modal_shown);
|
||||
useArrows(onNext, onPrev, is_shown);
|
||||
|
||||
useEffect(() => {
|
||||
setOffset(0);
|
||||
|
@ -249,7 +252,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
|||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={classNames(styles.cutter, { [styles.is_loading]: is_loading })} ref={wrap}>
|
||||
<div className={classNames(styles.cutter, { [styles.is_loading]: isLoading })} ref={wrap}>
|
||||
<div
|
||||
className={classNames(styles.image_container, { [styles.is_dragging]: is_dragging })}
|
||||
style={{
|
||||
|
@ -261,7 +264,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
|||
onTouchStart={startDragging}
|
||||
ref={slide}
|
||||
>
|
||||
{!is_loading &&
|
||||
{!isLoading &&
|
||||
images.map((file, index) => (
|
||||
<div
|
||||
className={classNames(styles.image_wrap, {
|
||||
|
|
|
@ -2,8 +2,6 @@ import React, { FC } from 'react';
|
|||
|
||||
interface IProps {}
|
||||
|
||||
const NodeImageSwiperBlock: FC<IProps> = () => (
|
||||
<div>SWIPER</div>
|
||||
)
|
||||
const NodeImageSwiperBlock: FC<IProps> = () => <div>SWIPER</div>;
|
||||
|
||||
export { NodeImageSwiperBlock };
|
||||
|
|
|
@ -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<INode>;
|
||||
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<IProps> = memo(
|
||||
({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => {
|
||||
const [stack, setStack] = useState(false);
|
||||
const NodePanel: FC<IProps> = memo(({ node, isLoading }) => {
|
||||
const [can_edit, can_like, can_star] = useNodePermissions(node);
|
||||
const { onEdit, onLike, onStar, onLock } = useNodeActions(node);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(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 (
|
||||
<div className={styles.place} ref={ref}>
|
||||
{/*
|
||||
stack &&
|
||||
createPortal(
|
||||
<NodePanelInner
|
||||
node={node}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
onLock={onLock}
|
||||
is_loading={is_loading}
|
||||
stack
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
*/}
|
||||
|
||||
<NodePanelInner
|
||||
node={node}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
onLock={onLock}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
is_loading={!!is_loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className={styles.place}>
|
||||
<NodePanelInner
|
||||
node={node}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
onLock={onLock}
|
||||
canEdit={can_edit}
|
||||
canLike={can_like}
|
||||
canStar={can_star}
|
||||
isLoading={!!isLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, shallowEqual);
|
||||
|
||||
export { NodePanel };
|
||||
|
|
|
@ -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<INode>;
|
||||
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<IProps> = 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<IProps> = memo(
|
|||
<div className={styles.content}>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.title}>
|
||||
{is_loading ? <Placeholder width="40%" /> : title || '...'}
|
||||
{isLoading ? <Placeholder width="40%" /> : title || '...'}
|
||||
</div>
|
||||
|
||||
{user && user.username && (
|
||||
<div className={styles.name}>
|
||||
{is_loading ? (
|
||||
{isLoading ? (
|
||||
<Placeholder width="100px" />
|
||||
) : (
|
||||
`~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}`
|
||||
|
@ -59,14 +57,14 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
)}
|
||||
</div>
|
||||
|
||||
{can_edit && (
|
||||
{canEdit && (
|
||||
<div className={styles.editor_menu}>
|
||||
<div className={styles.editor_menu_button}>
|
||||
<Icon icon="dots-vertical" size={24} />
|
||||
</div>
|
||||
|
||||
<div className={styles.editor_buttons}>
|
||||
{can_star && (
|
||||
{canStar && (
|
||||
<div className={classNames(styles.star, { is_heroic })}>
|
||||
{is_heroic ? (
|
||||
<Icon icon="star_full" size={24} onClick={onStar} />
|
||||
|
@ -88,7 +86,7 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
)}
|
||||
|
||||
<div className={styles.buttons}>
|
||||
{can_like && (
|
||||
{canLike && (
|
||||
<div className={classNames(styles.like, { is_liked })}>
|
||||
{is_liked ? (
|
||||
<Icon icon="heart_full" size={24} onClick={onLike} />
|
||||
|
|
44
src/components/node/NodeRelatedBlock/index.tsx
Normal file
44
src/components/node/NodeRelatedBlock/index.tsx
Normal file
|
@ -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<IProps> = ({ isLoading, node, related }) => {
|
||||
if (isLoading) {
|
||||
return <NodeRelatedPlaceholder />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{related &&
|
||||
related.albums &&
|
||||
!!node?.id &&
|
||||
Object.keys(related.albums)
|
||||
.filter(album => related.albums[album].length > 0)
|
||||
.map(album => (
|
||||
<NodeRelated
|
||||
title={
|
||||
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>{album}</Link>
|
||||
}
|
||||
items={related.albums[album]}
|
||||
key={album}
|
||||
/>
|
||||
))}
|
||||
|
||||
{related && related.similar && related.similar.length > 0 && (
|
||||
<NodeRelated title="ПОХОЖИЕ" items={related.similar} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { NodeRelatedBlock };
|
52
src/components/node/NodeTagsBlock/index.tsx
Normal file
52
src/components/node/NodeTagsBlock/index.tsx
Normal file
|
@ -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<IProps> = ({ 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<ITag>) => {
|
||||
if (!node?.id || !tag?.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
|
||||
},
|
||||
[history, node]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeTags
|
||||
is_editable={is_user}
|
||||
tags={node.tags}
|
||||
onChange={onTagsChange}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { NodeTagsBlock };
|
|
@ -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 mapStateToProps> &
|
||||
typeof mapDispatchToProps &
|
||||
RouteComponentProps<{ id: string }> & {};
|
||||
|
||||
const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||
const NodeLayout: FC<IProps> = 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<ITag>) => {
|
||||
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<INodeComponentProps>) =>
|
||||
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}
|
||||
|
||||
<Container>
|
||||
<Card className={styles.node} seamless>
|
||||
{!!block && createNodeBlock(block)}
|
||||
{block}
|
||||
|
||||
<NodePanel
|
||||
node={pick(
|
||||
[
|
||||
'title',
|
||||
'user',
|
||||
'is_liked',
|
||||
'is_heroic',
|
||||
'deleted_at',
|
||||
'created_at',
|
||||
'like_count',
|
||||
],
|
||||
node
|
||||
)}
|
||||
layout={layout}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
onLock={onLock}
|
||||
is_loading={is_loading}
|
||||
<NodePanel node={current} isLoading={is_loading} />
|
||||
|
||||
<NodeBottomBlock
|
||||
node={current}
|
||||
isLoadingComments={is_loading_comments}
|
||||
comments={comments}
|
||||
isLoading={is_loading}
|
||||
commentsCount={comment_count}
|
||||
commentsOrder="DESC"
|
||||
related={related}
|
||||
/>
|
||||
|
||||
{node.deleted_at ? (
|
||||
<NodeDeletedBadge />
|
||||
) : (
|
||||
<Group>
|
||||
<Padder>
|
||||
<Group horizontal className={styles.content}>
|
||||
<Group className={styles.comments}>
|
||||
{inline && <div className={styles.inline}>{createNodeBlock(inline)}</div>}
|
||||
|
||||
{is_loading || is_loading_comments || (!comments.length && !inline) ? (
|
||||
<NodeNoComments is_loading={is_loading_comments || is_loading} />
|
||||
) : (
|
||||
<NodeComments
|
||||
count={comment_count}
|
||||
comments={comments}
|
||||
user={user}
|
||||
order="DESC"
|
||||
/>
|
||||
)}
|
||||
|
||||
{is_user && !is_loading && <NodeCommentForm nodeId={node.id} />}
|
||||
</Group>
|
||||
|
||||
<div className={styles.panel}>
|
||||
<Sticky>
|
||||
<Group style={{ flex: 1, minWidth: 0 }}>
|
||||
{!is_loading && (
|
||||
<NodeTags
|
||||
is_editable={is_user}
|
||||
tags={node.tags}
|
||||
onChange={onTagsChange}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{is_loading && <NodeRelatedPlaceholder />}
|
||||
|
||||
{!is_loading &&
|
||||
related &&
|
||||
related.albums &&
|
||||
!!node?.id &&
|
||||
Object.keys(related.albums)
|
||||
.filter(album => related.albums[album].length > 0)
|
||||
.map(album => (
|
||||
<NodeRelated
|
||||
title={
|
||||
<Link
|
||||
to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}
|
||||
>
|
||||
{album}
|
||||
</Link>
|
||||
}
|
||||
items={related.albums[album]}
|
||||
key={album}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!is_loading &&
|
||||
related &&
|
||||
related.similar &&
|
||||
related.similar.length > 0 && (
|
||||
<NodeRelated title="ПОХОЖИЕ" items={related.similar} />
|
||||
)}
|
||||
</Group>
|
||||
</Sticky>
|
||||
</div>
|
||||
</Group>
|
||||
</Padder>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</Card>
|
||||
</Container>
|
||||
|
@ -259,6 +69,4 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
}
|
||||
);
|
||||
|
||||
const NodeLayout = connect(mapStateToProps, mapDispatchToProps)(NodeLayoutUnconnected);
|
||||
|
||||
export { NodeLayout, NodeLayoutUnconnected };
|
||||
export { NodeLayout };
|
||||
|
|
|
@ -129,7 +129,7 @@ export const nodeSetEditor = (editor: INode) => ({
|
|||
editor,
|
||||
});
|
||||
|
||||
export const nodeSetCoverImage = (current_cover_image: IFile) => ({
|
||||
export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
|
||||
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
||||
current_cover_image,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, ReactElement } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { IComment, INode, ValueOf } from '../types';
|
||||
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
|
||||
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
|
||||
|
@ -12,7 +12,6 @@ import { AudioEditor } from '~/components/editors/AudioEditor';
|
|||
import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton';
|
||||
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
|
||||
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||
import { modalShowPhotoswipe } from '../modal/actions';
|
||||
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
||||
import { EditorFiller } from '~/components/editors/EditorFiller';
|
||||
|
||||
|
@ -76,11 +75,7 @@ export const NODE_TYPES = {
|
|||
|
||||
export type INodeComponentProps = {
|
||||
node: INode;
|
||||
is_loading: boolean;
|
||||
is_modal_shown: boolean;
|
||||
layout: {};
|
||||
updateLayout: () => void;
|
||||
modalShowPhotoswipe: typeof modalShowPhotoswipe;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { createReducer } from '~/utils/reducer';
|
||||
import { INode, IComment, IFile } from '../types';
|
||||
import { EMPTY_NODE, EMPTY_COMMENT } from './constants';
|
||||
import { IComment, IFile, INode } from '../types';
|
||||
import { EMPTY_COMMENT, EMPTY_NODE } from './constants';
|
||||
import { NODE_HANDLERS } from './handlers';
|
||||
import { INodeRelated } from '~/redux/node/types';
|
||||
|
||||
export type INodeState = Readonly<{
|
||||
editor: INode;
|
||||
current: INode;
|
||||
comments: IComment[];
|
||||
related: {
|
||||
albums: Record<string, INode[]>;
|
||||
similar: INode[];
|
||||
};
|
||||
related: INodeRelated;
|
||||
comment_data: Record<number, IComment>;
|
||||
comment_count: number;
|
||||
current_cover_image?: IFile;
|
||||
|
|
|
@ -89,3 +89,8 @@ export type NodeEditorProps = {
|
|||
temp: string[];
|
||||
setTemp: (val: string[]) => void;
|
||||
};
|
||||
|
||||
export type INodeRelated = {
|
||||
albums: Record<string, INode[]>;
|
||||
similar: INode[];
|
||||
};
|
||||
|
|
13
src/utils/hooks/node/useLoadNode.ts
Normal file
13
src/utils/hooks/node/useLoadNode.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useEffect } from 'react';
|
||||
import { nodeGotoNode } from '~/redux/node/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
// useLoadNode loads node on id change
|
||||
export const useLoadNode = (id: any, isLoading: boolean) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
||||
}, [dispatch, id]);
|
||||
};
|
19
src/utils/hooks/node/useNodeActions.ts
Normal file
19
src/utils/hooks/node/useNodeActions.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions';
|
||||
|
||||
export const useNodeActions = (node: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onEdit = useCallback(() => dispatch(nodeEdit(node.id)), [dispatch, nodeEdit, node]);
|
||||
const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, nodeLike, node]);
|
||||
const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, nodeStar, node]);
|
||||
const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [
|
||||
dispatch,
|
||||
nodeLock,
|
||||
node,
|
||||
]);
|
||||
|
||||
return { onEdit, onLike, onStar, onLock };
|
||||
};
|
34
src/utils/hooks/node/useNodeBlocks.ts
Normal file
34
src/utils/hooks/node/useNodeBlocks.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { createElement, FC, useCallback, useMemo } from 'react';
|
||||
import { isNil, prop } from 'ramda';
|
||||
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/redux/node/constants';
|
||||
|
||||
// useNodeBlocks returns head, block and inline blocks of node
|
||||
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||
const createNodeBlock = useCallback(
|
||||
(block?: FC<INodeComponentProps>) =>
|
||||
!isNil(block) &&
|
||||
createElement(block, {
|
||||
node,
|
||||
isLoading,
|
||||
}),
|
||||
[node, isLoading]
|
||||
);
|
||||
|
||||
const head = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_HEADS) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
const block = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_COMPONENTS) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
const inline = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_INLINES) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
return { head, block, inline };
|
||||
};
|
17
src/utils/hooks/node/useNodeCoverImage.ts
Normal file
17
src/utils/hooks/node/useNodeCoverImage.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { nodeSetCoverImage } from '~/redux/node/actions';
|
||||
|
||||
export const useNodeCoverImage = (node: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.cover) return;
|
||||
dispatch(nodeSetCoverImage(node.cover));
|
||||
|
||||
return () => {
|
||||
nodeSetCoverImage(undefined);
|
||||
};
|
||||
}, [dispatch, node.cover]);
|
||||
};
|
14
src/utils/hooks/node/useNodePermissions.ts
Normal file
14
src/utils/hooks/node/useNodePermissions.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { useMemo } from 'react';
|
||||
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
export const useNodePermissions = (node: INode) => {
|
||||
const user = useShallowSelect(selectUser);
|
||||
const edit = useMemo(() => canEditNode(node, user), [node, user]);
|
||||
const like = useMemo(() => canLikeNode(node, user), [node, user]);
|
||||
const star = useMemo(() => canStarNode(node, user), [node, user]);
|
||||
|
||||
return [edit, like, star];
|
||||
};
|
7
src/utils/hooks/useScrollToTop.ts
Normal file
7
src/utils/hooks/useScrollToTop.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const useScrollToTop = (deps?: any[]) => {
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, deps || []);
|
||||
};
|
4
src/utils/hooks/user/userUser.ts
Normal file
4
src/utils/hooks/user/userUser.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
|
||||
export const useUser = () => useShallowSelect(selectUser);
|
Loading…
Add table
Add a link
Reference in a new issue