1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

loading node with hook

This commit is contained in:
Fedor Katurov 2021-09-20 11:26:20 +07:00
parent 35ce593ed8
commit 5e1e575ee3
19 changed files with 117 additions and 66 deletions

View file

@ -38,6 +38,7 @@
"redux-saga": "^1.1.1", "redux-saga": "^1.1.1",
"sticky-sidebar": "^3.3.1", "sticky-sidebar": "^3.3.1",
"swiper": "^6.7.0", "swiper": "^6.7.0",
"swr": "^1.0.1",
"throttle-debounce": "^2.1.0", "throttle-debounce": "^2.1.0",
"typescript": "^4.0.5", "typescript": "^4.0.5",
"typograf": "^6.11.3", "typograf": "^6.11.3",

View file

@ -14,7 +14,7 @@ import StickyBox from 'react-sticky-box/dist/esnext';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface IProps { interface IProps {
node: INode; node?: INode;
isLoading: boolean; isLoading: boolean;
commentsOrder: 'ASC' | 'DESC'; commentsOrder: 'ASC' | 'DESC';
comments: IComment[]; comments: IComment[];
@ -35,7 +35,7 @@ const NodeBottomBlock: FC<IProps> = ({
const { inline } = useNodeBlocks(node, isLoading); const { inline } = useNodeBlocks(node, isLoading);
const { is_user } = useUser(); const { is_user } = useUser();
if (node.deleted_at) { if (node?.deleted_at) {
return <NodeDeletedBadge />; return <NodeDeletedBadge />;
} }
@ -55,7 +55,7 @@ const NodeBottomBlock: FC<IProps> = ({
node={node} node={node}
/> />
{is_user && !isLoading && <NodeCommentForm nodeId={node.id} />} {is_user && !isLoading && <NodeCommentForm nodeId={node?.id} />}
</Group> </Group>
<div className={styles.panel}> <div className={styles.panel}>

View file

@ -7,7 +7,7 @@ import { useUser } from '~/utils/hooks/user/userUser';
interface IProps { interface IProps {
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
node: INode; node?: INode;
comments: IComment[]; comments: IComment[];
count: number; count: number;
isLoading: boolean; isLoading: boolean;

View file

@ -7,7 +7,7 @@ import { useNodeActions } from '~/utils/hooks/node/useNodeActions';
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
interface IProps { interface IProps {
node: INode; node?: INode;
isLoading: boolean; isLoading: boolean;
} }

View file

@ -9,7 +9,7 @@ import { URLS } from '~/constants/urls';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface IProps { interface IProps {
node: Partial<INode>; node?: Partial<INode>;
stack?: boolean; stack?: boolean;
canEdit: boolean; canEdit: boolean;
@ -26,8 +26,8 @@ interface IProps {
const NodePanelInner: FC<IProps> = memo( const NodePanelInner: FC<IProps> = memo(
({ ({
node: { id, title, user, is_liked, is_heroic, deleted_at, created_at, like_count },
stack, stack,
node,
canStar, canStar,
canEdit, canEdit,
@ -45,15 +45,15 @@ const NodePanelInner: FC<IProps> = memo(
<div className={styles.content}> <div className={styles.content}>
<div className={styles.panel}> <div className={styles.panel}>
<div className={styles.title}> <div className={styles.title}>
{isLoading ? <Placeholder width="40%" /> : title || '...'} {isLoading ? <Placeholder width="40%" /> : node?.title || '...'}
</div> </div>
{user && user.username && ( {node?.user && node?.user.username && (
<div className={styles.name}> <div className={styles.name}>
{isLoading ? ( {isLoading ? (
<Placeholder width="100px" /> <Placeholder width="100px" />
) : ( ) : (
`~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}` `~${node?.user.username.toLocaleLowerCase()}, ${getPrettyDate(node?.created_at)}`
)} )}
</div> </div>
)} )}
@ -67,8 +67,8 @@ const NodePanelInner: FC<IProps> = memo(
<div className={styles.editor_buttons}> <div className={styles.editor_buttons}>
{canStar && ( {canStar && (
<div className={classNames(styles.star, { is_heroic })}> <div className={classNames(styles.star, { is_heroic: node?.is_heroic })}>
{is_heroic ? ( {node?.is_heroic ? (
<Icon icon="star_full" size={24} onClick={onStar} /> <Icon icon="star_full" size={24} onClick={onStar} />
) : ( ) : (
<Icon icon="star" size={24} onClick={onStar} /> <Icon icon="star" size={24} onClick={onStar} />
@ -77,10 +77,14 @@ const NodePanelInner: FC<IProps> = memo(
)} )}
<div> <div>
<Icon icon={deleted_at ? 'locked' : 'unlocked'} size={24} onClick={onLock} /> <Icon
icon={node?.deleted_at ? 'locked' : 'unlocked'}
size={24}
onClick={onLock}
/>
</div> </div>
<Link to={URLS.NODE_EDIT_URL(id)}> <Link to={URLS.NODE_EDIT_URL(node?.id)}>
<Icon icon="edit" size={24} onClick={onEdit} /> <Icon icon="edit" size={24} onClick={onEdit} />
</Link> </Link>
</div> </div>
@ -89,15 +93,15 @@ const NodePanelInner: FC<IProps> = memo(
<div className={styles.buttons}> <div className={styles.buttons}>
{canLike && ( {canLike && (
<div className={classNames(styles.like, { is_liked })}> <div className={classNames(styles.like, { is_liked: node?.is_liked })}>
{is_liked ? ( {node?.is_liked ? (
<Icon icon="heart_full" size={24} onClick={onLike} /> <Icon icon="heart_full" size={24} onClick={onLike} />
) : ( ) : (
<Icon icon="heart" size={24} onClick={onLike} /> <Icon icon="heart" size={24} onClick={onLike} />
)} )}
{!!like_count && like_count > 0 && ( {!!node?.like_count && node.like_count > 0 && (
<div className={styles.like_count}>{like_count}</div> <div className={styles.like_count}>{node.like_count}</div>
)} )}
</div> </div>
)} )}

View file

@ -8,12 +8,12 @@ import { Link } from 'react-router-dom';
interface IProps { interface IProps {
isLoading: boolean; isLoading: boolean;
node: INode; node?: INode;
related: INodeRelated; related: INodeRelated;
} }
const NodeRelatedBlock: FC<IProps> = ({ isLoading, node, related }) => { const NodeRelatedBlock: FC<IProps> = ({ isLoading, node, related }) => {
if (isLoading) { if (isLoading || !node?.id) {
return <NodeRelatedPlaceholder />; return <NodeRelatedPlaceholder />;
} }

View file

@ -4,12 +4,12 @@ import { Tags } from '~/components/tags/Tags';
interface IProps { interface IProps {
is_editable?: boolean; is_editable?: boolean;
tags: ITag[]; tags?: ITag[];
onChange?: (tags: string[]) => void; onChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void; onTagClick?: (tag: Partial<ITag>) => void;
} }
const NodeTags: FC<IProps> = memo(({ is_editable, tags, onChange, onTagClick }) => { const NodeTags: FC<IProps> = memo(({ is_editable, tags = [], onChange, onTagClick }) => {
return ( return (
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} onTagClick={onTagClick} /> <Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} onTagClick={onTagClick} />
); );

View file

@ -8,8 +8,8 @@ import { NodeTags } from '~/components/node/NodeTags';
import { useUser } from '~/utils/hooks/user/userUser'; import { useUser } from '~/utils/hooks/user/userUser';
interface IProps { interface IProps {
node: INode; node?: INode;
isLoading: boolean; isLoading?: boolean;
} }
const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => { const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
@ -19,7 +19,7 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
const onTagsChange = useCallback( const onTagsChange = useCallback(
(tags: string[]) => { (tags: string[]) => {
dispatch(nodeUpdateTags(node.id, tags)); dispatch(nodeUpdateTags(node?.id, tags));
}, },
[dispatch, node] [dispatch, node]
); );
@ -42,7 +42,7 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
return ( return (
<NodeTags <NodeTags
is_editable={is_user} is_editable={is_user}
tags={node.tags} tags={node?.tags}
onChange={onTagsChange} onChange={onTagsChange}
onTagClick={onTagClick} onTagClick={onTagClick}
/> />

View file

@ -6,7 +6,6 @@ import { Card } from '~/components/containers/Card';
import { NodePanel } from '~/components/node/NodePanel'; import { NodePanel } from '~/components/node/NodePanel';
import { Footer } from '~/components/main/Footer'; import { Footer } from '~/components/main/Footer';
import styles from './styles.module.scss';
import { SidebarRouter } from '~/containers/main/SidebarRouter'; import { SidebarRouter } from '~/containers/main/SidebarRouter';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { Container } from '~/containers/main/Container'; import { Container } from '~/containers/main/Container';
@ -19,6 +18,9 @@ import { URLS } from '~/constants/urls';
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog'; import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen'; import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen';
import styles from './styles.module.scss';
import { useNodeFetcher } from '~/utils/hooks/node/useNodeFetcher';
type IProps = RouteComponentProps<{ id: string }> & {}; type IProps = RouteComponentProps<{ id: string }> & {};
const NodeLayout: FC<IProps> = memo( const NodeLayout: FC<IProps> = memo(
@ -27,21 +29,16 @@ const NodeLayout: FC<IProps> = memo(
params: { id }, params: { id },
}, },
}) => { }) => {
const { const { node, isLoading } = useNodeFetcher(parseInt(id, 10));
is_loading,
current,
comments,
comment_count,
is_loading_comments,
related,
} = useShallowSelect(selectNode);
useNodeCoverImage(current); const { comments, comment_count, is_loading_comments, related } = useShallowSelect(selectNode);
useNodeCoverImage(node);
useScrollToTop([id]); useScrollToTop([id]);
useLoadNode(id, is_loading); useOnNodeSeen(node);
useOnNodeSeen(current); useLoadNode(id, isLoading);
const { head, block } = useNodeBlocks(current, is_loading); const { head, block } = useNodeBlocks(node, isLoading);
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
@ -51,13 +48,13 @@ const NodeLayout: FC<IProps> = memo(
<Card className={styles.node} seamless> <Card className={styles.node} seamless>
{block} {block}
<NodePanel node={current} isLoading={is_loading} /> <NodePanel node={node} isLoading={isLoading} />
<NodeBottomBlock <NodeBottomBlock
node={current} node={node}
isLoadingComments={is_loading_comments} isLoadingComments={is_loading_comments}
comments={comments} comments={comments}
isLoading={is_loading} isLoading={isLoading}
commentsCount={comment_count} commentsCount={comment_count}
commentsOrder="DESC" commentsOrder="DESC"
related={related} related={related}
@ -69,7 +66,7 @@ const NodeLayout: FC<IProps> = memo(
<SidebarRouter prefix="/post:id" /> <SidebarRouter prefix="/post:id" />
<Route path={URLS.NODE_EDIT_URL(':id')} component={EditorEditDialog} /> <Route path={URLS.NODE_EDIT_URL(':id')} render={() => <EditorEditDialog />} />
</div> </div>
); );
} }

View file

@ -66,8 +66,8 @@ export const getNodeDiff = ({
}) })
.then(cleanResult); .then(cleanResult);
export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) => export const apiGetNode = (id: INode['id']) =>
api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id), config).then(cleanResult); api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id!)).then(cleanResult);
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => { export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
const cancelToken = axios.CancelToken.source(); const cancelToken = axios.CancelToken.source();

View file

@ -149,7 +149,7 @@ function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
yield put(nodeSetLoading(true)); yield put(nodeSetLoading(true));
yield put(nodeSetLoadingComments(true)); yield put(nodeSetLoadingComments(true));
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id }); const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, id);
yield put(nodeSetCurrent(node)); yield put(nodeSetCurrent(node));
yield put(nodeSetLoading(false)); yield put(nodeSetLoading(false));
@ -240,7 +240,7 @@ function* onEditSaga({ id }: ReturnType<typeof nodeEdit>) {
yield put(modalShowDialog(DIALOGS.LOADING)); yield put(modalShowDialog(DIALOGS.LOADING));
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id }); const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, id);
if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return; if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return;

View file

@ -3,17 +3,33 @@ import { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions';
export const useNodeActions = (node: INode) => { export const useNodeActions = (node?: INode) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const onEdit = useCallback(() => dispatch(nodeEdit(node.id)), [dispatch, nodeEdit, node]); const onEdit = useCallback(() => {
const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, nodeLike, node]); if (!node?.id) {
const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, nodeStar, node]); return;
const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [ }
dispatch, dispatch(nodeEdit(node.id));
nodeLock, }, [node]);
node, const onLike = useCallback(() => {
]); if (!node?.id) {
return;
}
dispatch(nodeLike(node.id));
}, [node]);
const onStar = useCallback(() => {
if (!node?.id) {
return;
}
dispatch(nodeStar(node.id));
}, [node]);
const onLock = useCallback(() => {
if (!node?.id) {
return;
}
dispatch(nodeLock(node.id, !node.deleted_at));
}, [node]);
return { onEdit, onLike, onStar, onLock }; return { onEdit, onLike, onStar, onLock };
}; };

View file

@ -10,13 +10,14 @@ import {
} from '~/redux/node/constants'; } from '~/redux/node/constants';
// useNodeBlocks returns head, block and inline blocks of node // useNodeBlocks returns head, block and inline blocks of node
export const useNodeBlocks = (node: INode, isLoading: boolean) => { export const useNodeBlocks = (node?: INode, isLoading?: boolean) => {
const createNodeBlock = useCallback( const createNodeBlock = useCallback(
(block?: FC<INodeComponentProps>, key = 0) => (block?: FC<INodeComponentProps>, key = 0) =>
!isNil(node) &&
!isNil(block) && !isNil(block) &&
createElement(block, { createElement(block, {
node, node,
isLoading, isLoading: !!isLoading,
key: `${node.id}-${key}`, key: `${node.id}-${key}`,
}), }),
[node, isLoading] [node, isLoading]

View file

@ -3,14 +3,19 @@ import { useEffect } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { nodeSetCoverImage } from '~/redux/node/actions'; import { nodeSetCoverImage } from '~/redux/node/actions';
export const useNodeCoverImage = (node: INode) => { export const useNodeCoverImage = (node?: INode) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if (!node?.cover) {
dispatch(nodeSetCoverImage(undefined));
return;
}
dispatch(nodeSetCoverImage(node.cover)); dispatch(nodeSetCoverImage(node.cover));
return () => { return () => {
dispatch(nodeSetCoverImage(undefined)); dispatch(nodeSetCoverImage(undefined));
}; };
}, [dispatch, node.cover, node.id]); }, [node?.cover]);
}; };

View file

@ -0,0 +1,11 @@
import useSWR from 'swr';
import { INode } from '~/redux/types';
import { apiGetNode } from '~/redux/node/api';
export const useNodeFetcher = (id: INode['id']) => {
const { data, error, isValidating } = useSWR(`${id}`, apiGetNode);
const node = data?.node;
const isLoading = !node && !isValidating;
return { node, error, isLoading };
};

View file

@ -4,7 +4,7 @@ import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectUser } from '~/redux/auth/selectors'; import { selectUser } from '~/redux/auth/selectors';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
export const useNodePermissions = (node: INode) => { export const useNodePermissions = (node?: INode) => {
const user = useShallowSelect(selectUser); const user = useShallowSelect(selectUser);
const edit = useMemo(() => canEditNode(node, user), [node, user]); const edit = useMemo(() => canEditNode(node, user), [node, user]);
const like = useMemo(() => canLikeNode(node, user), [node, user]); const like = useMemo(() => canLikeNode(node, user), [node, user]);

View file

@ -4,9 +4,13 @@ import { labSeenNode } from '~/redux/lab/actions';
import { flowSeenNode } from '~/redux/flow/actions'; import { flowSeenNode } from '~/redux/flow/actions';
// useOnNodeSeen updates node seen status across all needed places // useOnNodeSeen updates node seen status across all needed places
export const useOnNodeSeen = (node: INode) => { export const useOnNodeSeen = (node?: INode) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
if (!node) {
return;
}
// Remove node from updated // Remove node from updated
if (node.is_promoted) { if (node.is_promoted) {
dispatch(flowSeenNode(node.id)); dispatch(flowSeenNode(node.id));

View file

@ -4,7 +4,7 @@ import { IUser } from '~/redux/auth/types';
import { path } from 'ramda'; import { path } from 'ramda';
import { NODE_TYPES } from '~/redux/node/constants'; import { NODE_TYPES } from '~/redux/node/constants';
export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean => export const canEditNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN || path(['role'], user) === USER_ROLES.ADMIN ||
(path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user)); (path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user));
@ -12,10 +12,10 @@ export const canEditComment = (comment: Partial<ICommentGroup>, user: Partial<IU
path(['role'], user) === USER_ROLES.ADMIN || path(['role'], user) === USER_ROLES.ADMIN ||
(path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user)); (path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user));
export const canLikeNode = (node: Partial<INode>, user: Partial<IUser>): boolean => export const canLikeNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST; path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean => export const canStarNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
(node.type === NODE_TYPES.IMAGE || node.is_promoted === false) && (node?.type === NODE_TYPES.IMAGE || node?.is_promoted === false) &&
path(['role'], user) && path(['role'], user) &&
path(['role'], user) === USER_ROLES.ADMIN; path(['role'], user) === USER_ROLES.ADMIN;

View file

@ -4001,6 +4001,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
des.js@^1.0.0: des.js@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -10881,6 +10886,13 @@ swiper@^6.7.0:
dom7 "^3.0.0" dom7 "^3.0.0"
ssr-window "^3.0.0" ssr-window "^3.0.0"
swr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/swr/-/swr-1.0.1.tgz#15f62846b87ee000e52fa07812bb65eb62d79483"
integrity sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==
dependencies:
dequal "2.0.2"
symbol-observable@^1.2.0: symbol-observable@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"