mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
added nodes heroize button
This commit is contained in:
parent
c49dbb344d
commit
6b5638b44e
11 changed files with 140 additions and 47 deletions
|
@ -3,18 +3,23 @@ import * as styles from './styles.scss';
|
|||
import { INode } from '~/redux/types';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
||||
import pick from 'ramda/es/pick';
|
||||
|
||||
interface IProps {
|
||||
node: INode;
|
||||
node: Partial<INode>;
|
||||
layout: {};
|
||||
|
||||
can_edit: boolean;
|
||||
can_like: boolean;
|
||||
can_star: boolean;
|
||||
|
||||
onEdit: () => void;
|
||||
onLike: () => void;
|
||||
onStar: () => void;
|
||||
}
|
||||
|
||||
const NodePanel: FC<IProps> = memo(({ node, layout, can_edit, can_like, onEdit, onLike }) => {
|
||||
const NodePanel: FC<IProps> = memo(
|
||||
({ node, layout, can_edit, can_like, can_star, onEdit, onLike, onStar }) => {
|
||||
const [stack, setStack] = useState(false);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
@ -48,8 +53,10 @@ const NodePanel: FC<IProps> = memo(({ node, layout, can_edit, can_like, onEdit,
|
|||
stack
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
|
@ -58,12 +65,15 @@ const NodePanel: FC<IProps> = memo(({ node, layout, can_edit, can_like, onEdit,
|
|||
node={node}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export { NodePanel };
|
||||
|
|
|
@ -7,20 +7,24 @@ import { INode } from '~/redux/types';
|
|||
import classNames from 'classnames';
|
||||
|
||||
interface IProps {
|
||||
node: INode;
|
||||
node: Partial<INode>;
|
||||
stack?: boolean;
|
||||
|
||||
can_edit: boolean;
|
||||
can_like: boolean;
|
||||
can_star: boolean;
|
||||
onEdit: () => void;
|
||||
onLike: () => void;
|
||||
onStar: () => void;
|
||||
}
|
||||
|
||||
const NodePanelInner: FC<IProps> = ({
|
||||
node: { title, user, is_liked },
|
||||
node: { title, user, is_liked, is_heroic },
|
||||
stack,
|
||||
can_star,
|
||||
can_edit,
|
||||
can_like,
|
||||
onStar,
|
||||
onEdit,
|
||||
onLike,
|
||||
}) => {
|
||||
|
@ -35,6 +39,15 @@ const NodePanelInner: FC<IProps> = ({
|
|||
</Group>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
{can_star && (
|
||||
<div className={classNames(styles.star, { is_heroic })}>
|
||||
{is_heroic ? (
|
||||
<Icon icon="star_full" size={24} onClick={onStar} />
|
||||
) : (
|
||||
<Icon icon="star" size={24} onClick={onStar} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{can_edit && (
|
||||
<div>
|
||||
<Icon icon="edit" size={24} onClick={onEdit} />
|
||||
|
|
|
@ -166,3 +166,18 @@
|
|||
animation: pulse 0.75s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
transition: fill, stroke 0.25s;
|
||||
will-change: transform;
|
||||
|
||||
&:global(.is_heroic) {
|
||||
svg {
|
||||
fill: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
fill: $orange;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,5 +15,6 @@ export const API = {
|
|||
COMMENT: (id: INode['id']) => `/node/${id}/comment`,
|
||||
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
|
||||
POST_LIKE: (id: INode['id']) => `/node/${id}/like`,
|
||||
POST_STAR: (id: INode['id']) => `/node/${id}/heroic`,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { canEditNode, canLikeNode } from '~/utils/node';
|
||||
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { NODE_COMPONENTS, NODE_INLINES } from '~/redux/node/constants';
|
|||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import { CommentForm } from '~/components/node/CommentForm';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
import pick from 'ramda/es/pick';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
node: selectNode(state),
|
||||
|
@ -29,6 +30,7 @@ const mapDispatchToProps = {
|
|||
nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage,
|
||||
nodeEdit: NODE_ACTIONS.nodeEdit,
|
||||
nodeLike: NODE_ACTIONS.nodeLike,
|
||||
nodeStar: NODE_ACTIONS.nodeStar,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> &
|
||||
|
@ -40,13 +42,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
node: { is_loading, is_loading_comments, comments = [], current: node, current_cover_image },
|
||||
node: { is_loading, is_loading_comments, comments = [], current: node },
|
||||
user,
|
||||
user: { is_user },
|
||||
nodeLoadNode,
|
||||
nodeUpdateTags,
|
||||
nodeEdit,
|
||||
nodeLike,
|
||||
nodeStar,
|
||||
nodeSetCoverImage,
|
||||
}) => {
|
||||
const [layout, setLayout] = useState({});
|
||||
|
@ -67,12 +70,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
|
||||
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 block = node && node.type && NODE_COMPONENTS[node.type];
|
||||
const inline_block = node && 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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.cover) return;
|
||||
|
@ -85,12 +90,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
|||
{block && createElement(block, { node, is_loading, updateLayout, layout })}
|
||||
|
||||
<NodePanel
|
||||
node={node}
|
||||
node={pick(['title', 'user', 'is_liked', 'is_heroic'], node)}
|
||||
layout={layout}
|
||||
can_edit={can_edit}
|
||||
can_like={can_like}
|
||||
can_star={can_star}
|
||||
onEdit={onEdit}
|
||||
onLike={onLike}
|
||||
onStar={onStar}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
|
|
|
@ -80,6 +80,11 @@ export const nodeLike = (id: INode['id']) => ({
|
|||
id,
|
||||
});
|
||||
|
||||
export const nodeStar = (id: INode['id']) => ({
|
||||
type: NODE_ACTIONS.STAR,
|
||||
id,
|
||||
});
|
||||
|
||||
export const nodeSetEditor = (editor: INode) => ({
|
||||
type: NODE_ACTIONS.SET_EDITOR,
|
||||
editor,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api';
|
||||
import { INode, IResultWithStatus, IComment } from '../types';
|
||||
import { API } from '~/constants/api';
|
||||
import { nodeUpdateTags } from './actions';
|
||||
import { nodeUpdateTags, nodeLike, nodeStar } from './actions';
|
||||
|
||||
export const postNode = ({
|
||||
access,
|
||||
|
@ -79,10 +79,21 @@ export const updateNodeTags = ({
|
|||
export const postNodeLike = ({
|
||||
id,
|
||||
access,
|
||||
}: ReturnType<typeof nodeUpdateTags> & { access: string }): Promise<
|
||||
}: ReturnType<typeof nodeLike> & { access: string }): Promise<
|
||||
IResultWithStatus<{ is_liked: INode['is_liked'] }>
|
||||
> =>
|
||||
api
|
||||
.post(API.NODE.POST_LIKE(id), {}, configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const postNodeStar = ({
|
||||
id,
|
||||
access,
|
||||
}: ReturnType<typeof nodeStar> & { access: string }): Promise<
|
||||
IResultWithStatus<{ is_liked: INode['is_liked'] }>
|
||||
> =>
|
||||
api
|
||||
.post(API.NODE.POST_STAR(id), {}, configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
|
|
@ -21,6 +21,7 @@ export const NODE_ACTIONS = {
|
|||
|
||||
EDIT: `${prefix}EDIT`,
|
||||
LIKE: `${prefix}LIKE`,
|
||||
STAR: `${prefix}STAR`,
|
||||
CREATE: `${prefix}CREATE`,
|
||||
|
||||
SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`,
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
getNodeComments,
|
||||
updateNodeTags,
|
||||
postNodeLike,
|
||||
postNodeStar,
|
||||
} from './api';
|
||||
import { reqWrapper } from '../auth/sagas';
|
||||
import { flowSetNodes } from '../flow/actions';
|
||||
|
@ -197,6 +198,21 @@ function* onLikeSaga({ id }: ReturnType<typeof nodeLike>) {
|
|||
yield call(updateNodeEverythere, { ...current, is_liked });
|
||||
}
|
||||
|
||||
function* onStarSaga({ id }: ReturnType<typeof nodeLike>) {
|
||||
const {
|
||||
current,
|
||||
current: { is_heroic },
|
||||
} = yield select(selectNode);
|
||||
|
||||
yield call(updateNodeEverythere, { ...current, is_heroic: !is_heroic });
|
||||
|
||||
const { data, error } = yield call(reqWrapper, postNodeStar, { id });
|
||||
|
||||
if (!error || data.is_heroic === !is_heroic) return; // ok and matches
|
||||
|
||||
yield call(updateNodeEverythere, { ...current, is_heroic });
|
||||
}
|
||||
|
||||
export default function* nodeSaga() {
|
||||
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave);
|
||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||
|
@ -205,4 +221,5 @@ export default function* nodeSaga() {
|
|||
yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga);
|
||||
yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga);
|
||||
yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga);
|
||||
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,16 @@ const Sprites: FC<{}> = () => (
|
|||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
|
||||
</g>
|
||||
|
||||
<g id="star" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z" />
|
||||
</g>
|
||||
|
||||
<g id="star_full" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
||||
</g>
|
||||
|
||||
<g id="heart" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z" />
|
||||
|
|
|
@ -9,3 +9,6 @@ export const canEditNode = (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 =>
|
||||
path(['role'], user) && path(['role'], user) === USER_ROLES.ADMIN;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue