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:
parent
35ce593ed8
commit
5e1e575ee3
19 changed files with 117 additions and 66 deletions
|
@ -38,6 +38,7 @@
|
|||
"redux-saga": "^1.1.1",
|
||||
"sticky-sidebar": "^3.3.1",
|
||||
"swiper": "^6.7.0",
|
||||
"swr": "^1.0.1",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"typescript": "^4.0.5",
|
||||
"typograf": "^6.11.3",
|
||||
|
|
|
@ -14,7 +14,7 @@ import StickyBox from 'react-sticky-box/dist/esnext';
|
|||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
node: INode;
|
||||
node?: INode;
|
||||
isLoading: boolean;
|
||||
commentsOrder: 'ASC' | 'DESC';
|
||||
comments: IComment[];
|
||||
|
@ -35,7 +35,7 @@ const NodeBottomBlock: FC<IProps> = ({
|
|||
const { inline } = useNodeBlocks(node, isLoading);
|
||||
const { is_user } = useUser();
|
||||
|
||||
if (node.deleted_at) {
|
||||
if (node?.deleted_at) {
|
||||
return <NodeDeletedBadge />;
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ const NodeBottomBlock: FC<IProps> = ({
|
|||
node={node}
|
||||
/>
|
||||
|
||||
{is_user && !isLoading && <NodeCommentForm nodeId={node.id} />}
|
||||
{is_user && !isLoading && <NodeCommentForm nodeId={node?.id} />}
|
||||
</Group>
|
||||
|
||||
<div className={styles.panel}>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useUser } from '~/utils/hooks/user/userUser';
|
|||
|
||||
interface IProps {
|
||||
order: 'ASC' | 'DESC';
|
||||
node: INode;
|
||||
node?: INode;
|
||||
comments: IComment[];
|
||||
count: number;
|
||||
isLoading: boolean;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useNodeActions } from '~/utils/hooks/node/useNodeActions';
|
|||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
interface IProps {
|
||||
node: INode;
|
||||
node?: INode;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { URLS } from '~/constants/urls';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface IProps {
|
||||
node: Partial<INode>;
|
||||
node?: Partial<INode>;
|
||||
stack?: boolean;
|
||||
|
||||
canEdit: boolean;
|
||||
|
@ -26,8 +26,8 @@ interface IProps {
|
|||
|
||||
const NodePanelInner: FC<IProps> = memo(
|
||||
({
|
||||
node: { id, title, user, is_liked, is_heroic, deleted_at, created_at, like_count },
|
||||
stack,
|
||||
node,
|
||||
|
||||
canStar,
|
||||
canEdit,
|
||||
|
@ -45,15 +45,15 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
<div className={styles.content}>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.title}>
|
||||
{isLoading ? <Placeholder width="40%" /> : title || '...'}
|
||||
{isLoading ? <Placeholder width="40%" /> : node?.title || '...'}
|
||||
</div>
|
||||
|
||||
{user && user.username && (
|
||||
{node?.user && node?.user.username && (
|
||||
<div className={styles.name}>
|
||||
{isLoading ? (
|
||||
<Placeholder width="100px" />
|
||||
) : (
|
||||
`~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}`
|
||||
`~${node?.user.username.toLocaleLowerCase()}, ${getPrettyDate(node?.created_at)}`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
@ -67,8 +67,8 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
|
||||
<div className={styles.editor_buttons}>
|
||||
{canStar && (
|
||||
<div className={classNames(styles.star, { is_heroic })}>
|
||||
{is_heroic ? (
|
||||
<div className={classNames(styles.star, { is_heroic: node?.is_heroic })}>
|
||||
{node?.is_heroic ? (
|
||||
<Icon icon="star_full" size={24} onClick={onStar} />
|
||||
) : (
|
||||
<Icon icon="star" size={24} onClick={onStar} />
|
||||
|
@ -77,10 +77,14 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
)}
|
||||
|
||||
<div>
|
||||
<Icon icon={deleted_at ? 'locked' : 'unlocked'} size={24} onClick={onLock} />
|
||||
<Icon
|
||||
icon={node?.deleted_at ? 'locked' : 'unlocked'}
|
||||
size={24}
|
||||
onClick={onLock}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Link to={URLS.NODE_EDIT_URL(id)}>
|
||||
<Link to={URLS.NODE_EDIT_URL(node?.id)}>
|
||||
<Icon icon="edit" size={24} onClick={onEdit} />
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -89,15 +93,15 @@ const NodePanelInner: FC<IProps> = memo(
|
|||
|
||||
<div className={styles.buttons}>
|
||||
{canLike && (
|
||||
<div className={classNames(styles.like, { is_liked })}>
|
||||
{is_liked ? (
|
||||
<div className={classNames(styles.like, { is_liked: node?.is_liked })}>
|
||||
{node?.is_liked ? (
|
||||
<Icon icon="heart_full" size={24} onClick={onLike} />
|
||||
) : (
|
||||
<Icon icon="heart" size={24} onClick={onLike} />
|
||||
)}
|
||||
|
||||
{!!like_count && like_count > 0 && (
|
||||
<div className={styles.like_count}>{like_count}</div>
|
||||
{!!node?.like_count && node.like_count > 0 && (
|
||||
<div className={styles.like_count}>{node.like_count}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -8,12 +8,12 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
interface IProps {
|
||||
isLoading: boolean;
|
||||
node: INode;
|
||||
node?: INode;
|
||||
related: INodeRelated;
|
||||
}
|
||||
|
||||
const NodeRelatedBlock: FC<IProps> = ({ isLoading, node, related }) => {
|
||||
if (isLoading) {
|
||||
if (isLoading || !node?.id) {
|
||||
return <NodeRelatedPlaceholder />;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ import { Tags } from '~/components/tags/Tags';
|
|||
|
||||
interface IProps {
|
||||
is_editable?: boolean;
|
||||
tags: ITag[];
|
||||
tags?: ITag[];
|
||||
onChange?: (tags: string[]) => 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 (
|
||||
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} onTagClick={onTagClick} />
|
||||
);
|
||||
|
|
|
@ -8,8 +8,8 @@ import { NodeTags } from '~/components/node/NodeTags';
|
|||
import { useUser } from '~/utils/hooks/user/userUser';
|
||||
|
||||
interface IProps {
|
||||
node: INode;
|
||||
isLoading: boolean;
|
||||
node?: INode;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
|
||||
|
@ -19,7 +19,7 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
|
|||
|
||||
const onTagsChange = useCallback(
|
||||
(tags: string[]) => {
|
||||
dispatch(nodeUpdateTags(node.id, tags));
|
||||
dispatch(nodeUpdateTags(node?.id, tags));
|
||||
},
|
||||
[dispatch, node]
|
||||
);
|
||||
|
@ -42,7 +42,7 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
|
|||
return (
|
||||
<NodeTags
|
||||
is_editable={is_user}
|
||||
tags={node.tags}
|
||||
tags={node?.tags}
|
||||
onChange={onTagsChange}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Card } from '~/components/containers/Card';
|
|||
import { NodePanel } from '~/components/node/NodePanel';
|
||||
import { Footer } from '~/components/main/Footer';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { Container } from '~/containers/main/Container';
|
||||
|
@ -19,6 +18,9 @@ import { URLS } from '~/constants/urls';
|
|||
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
|
||||
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 }> & {};
|
||||
|
||||
const NodeLayout: FC<IProps> = memo(
|
||||
|
@ -27,21 +29,16 @@ const NodeLayout: FC<IProps> = memo(
|
|||
params: { id },
|
||||
},
|
||||
}) => {
|
||||
const {
|
||||
is_loading,
|
||||
current,
|
||||
comments,
|
||||
comment_count,
|
||||
is_loading_comments,
|
||||
related,
|
||||
} = useShallowSelect(selectNode);
|
||||
const { node, isLoading } = useNodeFetcher(parseInt(id, 10));
|
||||
|
||||
useNodeCoverImage(current);
|
||||
const { comments, comment_count, is_loading_comments, related } = useShallowSelect(selectNode);
|
||||
|
||||
useNodeCoverImage(node);
|
||||
useScrollToTop([id]);
|
||||
useLoadNode(id, is_loading);
|
||||
useOnNodeSeen(current);
|
||||
useOnNodeSeen(node);
|
||||
useLoadNode(id, isLoading);
|
||||
|
||||
const { head, block } = useNodeBlocks(current, is_loading);
|
||||
const { head, block } = useNodeBlocks(node, isLoading);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
|
@ -51,13 +48,13 @@ const NodeLayout: FC<IProps> = memo(
|
|||
<Card className={styles.node} seamless>
|
||||
{block}
|
||||
|
||||
<NodePanel node={current} isLoading={is_loading} />
|
||||
<NodePanel node={node} isLoading={isLoading} />
|
||||
|
||||
<NodeBottomBlock
|
||||
node={current}
|
||||
node={node}
|
||||
isLoadingComments={is_loading_comments}
|
||||
comments={comments}
|
||||
isLoading={is_loading}
|
||||
isLoading={isLoading}
|
||||
commentsCount={comment_count}
|
||||
commentsOrder="DESC"
|
||||
related={related}
|
||||
|
@ -69,7 +66,7 @@ const NodeLayout: FC<IProps> = memo(
|
|||
|
||||
<SidebarRouter prefix="/post:id" />
|
||||
|
||||
<Route path={URLS.NODE_EDIT_URL(':id')} component={EditorEditDialog} />
|
||||
<Route path={URLS.NODE_EDIT_URL(':id')} render={() => <EditorEditDialog />} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,8 +66,8 @@ export const getNodeDiff = ({
|
|||
})
|
||||
.then(cleanResult);
|
||||
|
||||
export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) =>
|
||||
api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id), config).then(cleanResult);
|
||||
export const apiGetNode = (id: INode['id']) =>
|
||||
api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id!)).then(cleanResult);
|
||||
|
||||
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
|
|
|
@ -149,7 +149,7 @@ function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
|||
yield put(nodeSetLoading(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(nodeSetLoading(false));
|
||||
|
@ -240,7 +240,7 @@ function* onEditSaga({ id }: ReturnType<typeof nodeEdit>) {
|
|||
|
||||
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;
|
||||
|
||||
|
|
|
@ -3,17 +3,33 @@ import { useCallback } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions';
|
||||
|
||||
export const useNodeActions = (node: INode) => {
|
||||
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,
|
||||
]);
|
||||
const onEdit = useCallback(() => {
|
||||
if (!node?.id) {
|
||||
return;
|
||||
}
|
||||
dispatch(nodeEdit(node.id));
|
||||
}, [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 };
|
||||
};
|
||||
|
|
|
@ -10,13 +10,14 @@ import {
|
|||
} from '~/redux/node/constants';
|
||||
|
||||
// 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(
|
||||
(block?: FC<INodeComponentProps>, key = 0) =>
|
||||
!isNil(node) &&
|
||||
!isNil(block) &&
|
||||
createElement(block, {
|
||||
node,
|
||||
isLoading,
|
||||
isLoading: !!isLoading,
|
||||
key: `${node.id}-${key}`,
|
||||
}),
|
||||
[node, isLoading]
|
||||
|
|
|
@ -3,14 +3,19 @@ import { useEffect } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { nodeSetCoverImage } from '~/redux/node/actions';
|
||||
|
||||
export const useNodeCoverImage = (node: INode) => {
|
||||
export const useNodeCoverImage = (node?: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!node?.cover) {
|
||||
dispatch(nodeSetCoverImage(undefined));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(nodeSetCoverImage(node.cover));
|
||||
|
||||
return () => {
|
||||
dispatch(nodeSetCoverImage(undefined));
|
||||
};
|
||||
}, [dispatch, node.cover, node.id]);
|
||||
}, [node?.cover]);
|
||||
};
|
||||
|
|
11
src/utils/hooks/node/useNodeFetcher.ts
Normal file
11
src/utils/hooks/node/useNodeFetcher.ts
Normal 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 };
|
||||
};
|
|
@ -4,7 +4,7 @@ import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
|||
import { selectUser } from '~/redux/auth/selectors';
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
export const useNodePermissions = (node: INode) => {
|
||||
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]);
|
||||
|
|
|
@ -4,9 +4,13 @@ import { labSeenNode } from '~/redux/lab/actions';
|
|||
import { flowSeenNode } from '~/redux/flow/actions';
|
||||
|
||||
// useOnNodeSeen updates node seen status across all needed places
|
||||
export const useOnNodeSeen = (node: INode) => {
|
||||
export const useOnNodeSeen = (node?: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove node from updated
|
||||
if (node.is_promoted) {
|
||||
dispatch(flowSeenNode(node.id));
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IUser } from '~/redux/auth/types';
|
|||
import { path } from 'ramda';
|
||||
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(['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(['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;
|
||||
|
||||
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
||||
(node.type === NODE_TYPES.IMAGE || node.is_promoted === false) &&
|
||||
export const canStarNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
|
||||
(node?.type === NODE_TYPES.IMAGE || node?.is_promoted === false) &&
|
||||
path(['role'], user) &&
|
||||
path(['role'], user) === USER_ROLES.ADMIN;
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -4001,6 +4001,11 @@ depd@~1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue