diff --git a/src/components/node/NodePanel/index.tsx b/src/components/node/NodePanel/index.tsx deleted file mode 100644 index 0ab3c7f0..00000000 --- a/src/components/node/NodePanel/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { FC, memo } from 'react'; -import { INode } from '~/redux/types'; -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: INode; - isLoading: boolean; -} - -const NodePanel: FC = memo(({ node, isLoading }) => { - const [can_edit, can_like, can_star] = useNodePermissions(node); - const { onEdit, onLike, onStar, onLock } = useNodeActions(node); - - return ( - - ); -}, shallowEqual); - -export { NodePanel }; diff --git a/src/components/node/NodePanelInner/index.tsx b/src/components/node/NodePanelInner/index.tsx index ea717ccc..f04e259b 100644 --- a/src/components/node/NodePanelInner/index.tsx +++ b/src/components/node/NodePanelInner/index.tsx @@ -1,7 +1,6 @@ -import React, { FC, memo } from 'react'; +import React, { VFC, memo } from 'react'; import styles from './styles.module.scss'; import { Icon } from '~/components/input/Icon'; -import { INode } from '~/redux/types'; import classNames from 'classnames'; import { Placeholder } from '~/components/placeholders/Placeholder'; import { getPrettyDate } from '~/utils/dom'; @@ -9,8 +8,15 @@ import { URLS } from '~/constants/urls'; import { Link } from 'react-router-dom'; interface IProps { - node: Partial; - stack?: boolean; + id?: number; + title: string; + username?: string; + createdAt: string; + likeCount: number; + + isHeroic: boolean; + isLocked: boolean; + isLiked: boolean; canEdit: boolean; canLike: boolean; @@ -24,10 +30,17 @@ interface IProps { onLock: () => void; } -const NodePanelInner: FC = memo( +const NodePanelInner: VFC = memo( ({ - node: { id, title, user, is_liked, is_heroic, deleted_at, created_at, like_count }, - stack, + id, + title, + username, + createdAt, + likeCount, + + isHeroic, + isLocked, + isLiked, canStar, canEdit, @@ -41,19 +54,19 @@ const NodePanelInner: FC = memo( onLock, }) => { return ( -
+
{isLoading ? : title || '...'}
- {user && user.username && ( + {!!username && (
{isLoading ? ( ) : ( - `~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}` + `~${username.toLocaleLowerCase()}, ${getPrettyDate(createdAt)}` )}
)} @@ -67,8 +80,8 @@ const NodePanelInner: FC = memo(
{canStar && ( -
- {is_heroic ? ( +
+ {isHeroic ? ( ) : ( @@ -77,27 +90,29 @@ const NodePanelInner: FC = memo( )}
- +
- - - + {!!id && ( + + + + )}
)}
{canLike && ( -
- {is_liked ? ( +
+ {isLiked ? ( ) : ( )} - {!!like_count && like_count > 0 && ( -
{like_count}
+ {!!likeCount && likeCount > 0 && ( +
{likeCount}
)}
)} diff --git a/src/components/node/NodePanelInner/styles.module.scss b/src/components/node/NodePanelInner/styles.module.scss index e12e3742..45ee79cf 100644 --- a/src/components/node/NodePanelInner/styles.module.scss +++ b/src/components/node/NodePanelInner/styles.module.scss @@ -38,17 +38,6 @@ justify-content: center; box-sizing: border-box; min-width: 0; - - &:global(.stack) { - padding: 0 $gap; - bottom: 0; - position: fixed; - z-index: 5; - - @include tablet { - padding: 0; - } - } } .content { @@ -209,7 +198,7 @@ position: relative; flex: 0 0 32px; - &:global(.is_liked) { + .is_liked { svg { fill: $red; } @@ -249,7 +238,7 @@ transition: fill, stroke 0.25s; will-change: transform; - &:global(.is_heroic) { + .is_heroic { svg { fill: $orange; } diff --git a/src/layouts/NodeLayout/index.tsx b/src/layouts/NodeLayout/index.tsx index e57a7e66..f0a25f9e 100644 --- a/src/layouts/NodeLayout/index.tsx +++ b/src/layouts/NodeLayout/index.tsx @@ -2,7 +2,6 @@ import React, { FC } from 'react'; import { Route } from 'react-router'; import { Card } from '~/components/containers/Card'; -import { NodePanel } from '~/components/node/NodePanel'; import { Footer } from '~/components/main/Footer'; import { SidebarRouter } from '~/containers/main/SidebarRouter'; @@ -15,12 +14,17 @@ import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog'; import styles from './styles.module.scss'; import { useNodeContext } from '~/utils/context/NodeContextProvider'; +import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions'; +import { useNodeActions } from '~/utils/hooks/node/useNodeActions'; +import { NodePanelInner } from '~/components/node/NodePanelInner'; type IProps = {}; const NodeLayout: FC = () => { - const { node, isLoading } = useNodeContext(); + const { node, isLoading, update } = useNodeContext(); const { head, block } = useNodeBlocks(node, isLoading); + const [canEdit, canLike, canStar] = useNodePermissions(node); + const { onEdit, onLike, onStar, onLock } = useNodeActions(node, update); useNodeCoverImage(node); @@ -33,7 +37,24 @@ const NodeLayout: FC = () => { {block}
- +
diff --git a/src/pages/boris.tsx b/src/pages/boris.tsx index a9181b80..936b1b87 100644 --- a/src/pages/boris.tsx +++ b/src/pages/boris.tsx @@ -9,12 +9,12 @@ import { useImageModal } from '~/utils/hooks/useImageModal'; import { useNodeComments } from '~/utils/hooks/node/useNodeComments'; import { useBoris } from '~/utils/hooks/boris/useBoris'; import { NodeContextProvider } from '~/utils/context/NodeContextProvider'; +import { useGetNode } from '~/utils/hooks/data/useGetNode'; const BorisPage: VFC = () => { const dispatch = useDispatch(); + const { node, isLoading, update } = useGetNode(696); const { - current, - is_loading, comments, comment_count: count, is_loading_comments: isLoadingComments, @@ -28,7 +28,7 @@ const BorisPage: VFC = () => { }, [dispatch]); return ( - + = ({ params: { id }, }, }) => { - const { node, isLoading } = useGetNode(parseInt(id, 10)); + const { node, isLoading, update } = useGetNode(parseInt(id, 10)); const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id); const onShowImageModal = useImageModal(); @@ -40,7 +40,7 @@ const NodePage: FC = ({ } return ( - + ) => ({ type: NODE_ACTIONS.SET, }); -export const nodeSetSaveErrors = (errors: IValidationErrors) => ({ - errors, - type: NODE_ACTIONS.SET_SAVE_ERRORS, -}); - export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({ id, node_type, @@ -60,22 +55,6 @@ export const nodeSetComments = (comments: IComment[]) => ({ type: NODE_ACTIONS.SET_COMMENTS, }); -export const nodeLike = (id: INode['id']) => ({ - type: NODE_ACTIONS.LIKE, - id, -}); - -export const nodeStar = (id: INode['id']) => ({ - type: NODE_ACTIONS.STAR, - id, -}); - -export const nodeLock = (id: INode['id'], is_locked: boolean) => ({ - type: NODE_ACTIONS.LOCK, - id, - is_locked, -}); - export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) => ({ type: NODE_ACTIONS.LOCK_COMMENT, nodeId, @@ -83,11 +62,6 @@ export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) is_locked, }); -export const nodeSetEditor = (editor: INode) => ({ - type: NODE_ACTIONS.SET_EDITOR, - editor, -}); - export const nodeSetCoverImage = (current_cover_image?: IFile) => ({ type: NODE_ACTIONS.SET_COVER_IMAGE, current_cover_image, diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 314d7ad1..aad93140 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -29,23 +29,17 @@ export const NODE_ACTIONS = { GOTO_NODE: `${prefix}GOTO_NODE`, SET: `${prefix}SET`, - LIKE: `${prefix}LIKE`, - STAR: `${prefix}STAR`, - LOCK: `${prefix}LOCK`, LOCK_COMMENT: `${prefix}LOCK_COMMENT`, EDIT_COMMENT: `${prefix}EDIT_COMMENT`, LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`, - SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, SET_LOADING: `${prefix}SET_LOADING`, SET_LOADING_COMMENTS: `${prefix}SET_LOADING_COMMENTS`, SET_SENDING_COMMENT: `${prefix}SET_SENDING_COMMENT`, SET_CURRENT: `${prefix}SET_CURRENT`, - SET_EDITOR: `${prefix}SET_EDITOR`, POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`, SET_COMMENTS: `${prefix}SET_COMMENTS`, - SET_RELATED: `${prefix}SET_RELATED`, SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`, }; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index 87b6d9dd..1d86e87f 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -5,10 +5,8 @@ import { nodeSetComments, nodeSetCoverImage, nodeSetCurrent, - nodeSetEditor, nodeSetLoading, nodeSetLoadingComments, - nodeSetSaveErrors, nodeSetSendingComment, } from './actions'; import { INodeState } from './reducer'; @@ -18,9 +16,6 @@ const setData = (state: INodeState, { node }: ReturnType) => ({ ...node, }); -const setSaveErrors = (state: INodeState, { errors }: ReturnType) => - assocPath(['errors'], errors, state); - const setLoading = (state: INodeState, { is_loading }: ReturnType) => assocPath(['is_loading'], is_loading, state); @@ -40,9 +35,6 @@ const setSendingComment = ( const setComments = (state: INodeState, { comments }: ReturnType) => assocPath(['comments'], comments, state); -const setEditor = (state: INodeState, { editor }: ReturnType) => - assocPath(['editor'], editor, state); - const setCoverImage = ( state: INodeState, { current_cover_image }: ReturnType @@ -50,12 +42,10 @@ const setCoverImage = ( export const NODE_HANDLERS = { [NODE_ACTIONS.SET]: setData, - [NODE_ACTIONS.SET_SAVE_ERRORS]: setSaveErrors, [NODE_ACTIONS.SET_LOADING]: setLoading, [NODE_ACTIONS.SET_LOADING_COMMENTS]: setLoadingComments, [NODE_ACTIONS.SET_CURRENT]: setCurrent, [NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment, [NODE_ACTIONS.SET_COMMENTS]: setComments, - [NODE_ACTIONS.SET_EDITOR]: setEditor, [NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index df713aa0..1db3352e 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -1,11 +1,9 @@ import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects'; -import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants'; +import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS } from './constants'; import { nodeGotoNode, - nodeLike, nodeLoadNode, - nodeLock, nodeLockComment, nodePostLocalComment, nodeSet, @@ -13,27 +11,11 @@ import { nodeSetCurrent, nodeSetLoadingComments, } from './actions'; -import { - apiGetNodeComments, - apiLockComment, - apiLockNode, - apiPostComment, - apiPostNodeHeroic, - apiPostNodeLike, - apiPostNodeTags, -} from './api'; +import { apiGetNodeComments, apiLockComment, apiPostComment } from './api'; import { flowSetNodes } from '../flow/actions'; -import { modalSetShown, modalShowDialog } from '../modal/actions'; import { selectFlowNodes } from '../flow/selectors'; -import { URLS } from '~/constants/urls'; import { selectNode } from './selectors'; import { INode, Unwrap } from '../types'; -import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs'; -import { DIALOGS } from '~/redux/modal/constants'; -import { has } from 'ramda'; -import { selectLabListNodes } from '~/redux/lab/selectors'; -import { labSetList } from '~/redux/lab/actions'; -import { apiPostNode } from '~/redux/node/api'; import { showErrorToast } from '~/utils/errors/showToast'; export function* updateNodeEverywhere(node) { @@ -156,63 +138,6 @@ function* onPostComment({ nodeId, comment, callback }: ReturnType) { - const { current }: ReturnType = yield select(selectNode); - - try { - const count = current.like_count || 0; - - yield call(updateNodeEverywhere, { - ...current, - is_liked: !current.is_liked, - like_count: current.is_liked ? Math.max(count - 1, 0) : count + 1, - }); - - const data: Unwrap = yield call(apiPostNodeLike, { id }); - - yield call(updateNodeEverywhere, { - ...current, - is_liked: data.is_liked, - like_count: data.is_liked ? count + 1 : Math.max(count - 1, 0), - }); - } catch {} -} - -function* onStarSaga({ id }: ReturnType) { - try { - const { - current, - current: { is_heroic }, - } = yield select(selectNode); - - yield call(updateNodeEverywhere, { ...current, is_heroic: !is_heroic }); - - const data: Unwrap = yield call(apiPostNodeHeroic, { id }); - - yield call(updateNodeEverywhere, { ...current, is_heroic: data.is_heroic }); - } catch {} -} - -function* onLockSaga({ id, is_locked }: ReturnType) { - const { current }: ReturnType = yield select(selectNode); - - try { - yield call(updateNodeEverywhere, { - ...current, - deleted_at: is_locked ? new Date().toISOString() : null, - }); - - const data: Unwrap = yield call(apiLockNode, { id, is_locked }); - - yield call(updateNodeEverywhere, { - ...current, - deleted_at: data.deleted_at || undefined, - }); - } catch { - yield call(updateNodeEverywhere, { ...current, deleted_at: current.deleted_at }); - } -} - function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType) { const { comments }: ReturnType = yield select(selectNode); @@ -239,9 +164,6 @@ export default function* nodeSaga() { yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto); yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostComment); - yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga); - yield takeLatest(NODE_ACTIONS.STAR, onStarSaga); - yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga); yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga); yield takeLeading(NODE_ACTIONS.LOAD_MORE_COMMENTS, onNodeLoadMoreComments); } diff --git a/src/utils/context/NodeContextProvider.tsx b/src/utils/context/NodeContextProvider.tsx index b3e21715..a664323d 100644 --- a/src/utils/context/NodeContextProvider.tsx +++ b/src/utils/context/NodeContextProvider.tsx @@ -4,11 +4,13 @@ import React, { createContext, FC, useContext } from 'react'; export interface NodeContextProps { node: INode; + update: (node: Partial) => Promise; isLoading: boolean; } export const NodeContext = createContext({ node: EMPTY_NODE, + update: async () => {}, isLoading: false, }); diff --git a/src/utils/hooks/data/useGetNode.ts b/src/utils/hooks/data/useGetNode.ts index 0db9b865..547f8315 100644 --- a/src/utils/hooks/data/useGetNode.ts +++ b/src/utils/hooks/data/useGetNode.ts @@ -5,6 +5,7 @@ import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen'; import { apiGetNode } from '~/redux/node/api'; import { useCallback } from 'react'; import { INode } from '~/redux/types'; +import { EMPTY_NODE } from '~/redux/node/constants'; export const useGetNode = (id: number) => { const { data, isValidating, mutate } = useSWR(API.NODE.GET_NODE(id), () => @@ -25,5 +26,5 @@ export const useGetNode = (id: number) => { useOnNodeSeen(data?.node); - return { node: data?.node, isLoading: isValidating && !data, update }; + return { node: data?.node || EMPTY_NODE, isLoading: isValidating && !data, update }; }; diff --git a/src/utils/hooks/node/useNodeActions.ts b/src/utils/hooks/node/useNodeActions.ts index 04be1d72..795bd5da 100644 --- a/src/utils/hooks/node/useNodeActions.ts +++ b/src/utils/hooks/node/useNodeActions.ts @@ -1,11 +1,12 @@ import { INode } from '~/redux/types'; import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; import { modalShowDialog } from '~/redux/modal/actions'; import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs'; +import { apiLockNode, apiPostNodeHeroic, apiPostNodeLike } from '~/redux/node/api'; +import { showErrorToast } from '~/utils/errors/showToast'; -export const useNodeActions = (node: INode) => { +export const useNodeActions = (node: INode, update: (node: Partial) => Promise) => { const dispatch = useDispatch(); const onEdit = useCallback(() => { @@ -16,9 +17,38 @@ export const useNodeActions = (node: INode) => { dispatch(modalShowDialog(NODE_EDITOR_DIALOGS[node.type])); }, [dispatch, node]); - const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, node]); - const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, node]); - const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [dispatch, node]); + const onLike = useCallback(async () => { + try { + const result = await apiPostNodeLike({ id: node.id }); + const likeCount = node.like_count || 0; + + if (result.is_liked) { + await update({ like_count: likeCount + 1 }); + } else { + await update({ like_count: likeCount - 1 }); + } + } catch (error) { + showErrorToast(error); + } + }, [node.id, node.like_count, update]); + + const onStar = useCallback(async () => { + try { + const result = await apiPostNodeHeroic({ id: node.id }); + await update({ is_heroic: result.is_heroic }); + } catch (error) { + showErrorToast(error); + } + }, [node.id, update]); + + const onLock = useCallback(async () => { + try { + const result = await apiLockNode({ id: node.id, is_locked: !node.deleted_at }); + await update({ deleted_at: result.deleted_at }); + } catch (error) { + showErrorToast(error); + } + }, [node.deleted_at, node.id, update]); return { onEdit, onLike, onStar, onLock }; };