1
0
Fork 0
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:
Fedor Katurov 2019-10-23 12:02:35 +07:00
parent c49dbb344d
commit 6b5638b44e
11 changed files with 140 additions and 47 deletions

View file

@ -3,67 +3,77 @@ import * as styles from './styles.scss';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { NodePanelInner } from '~/components/node/NodePanelInner'; import { NodePanelInner } from '~/components/node/NodePanelInner';
import pick from 'ramda/es/pick';
interface IProps { interface IProps {
node: INode; node: Partial<INode>;
layout: {}; layout: {};
can_edit: boolean; can_edit: boolean;
can_like: boolean; can_like: boolean;
can_star: boolean;
onEdit: () => void; onEdit: () => void;
onLike: () => void; onLike: () => void;
onStar: () => void;
} }
const NodePanel: FC<IProps> = memo(({ node, layout, can_edit, can_like, onEdit, onLike }) => { const NodePanel: FC<IProps> = memo(
const [stack, setStack] = useState(false); ({ node, layout, can_edit, can_like, can_star, onEdit, onLike, onStar }) => {
const [stack, setStack] = useState(false);
const ref = useRef(null); const ref = useRef(null);
const getPlace = useCallback(() => { const getPlace = useCallback(() => {
if (!ref.current) return; if (!ref.current) return;
const { offsetTop } = ref.current; const { offsetTop } = ref.current;
const { height } = ref.current.getBoundingClientRect(); const { height } = ref.current.getBoundingClientRect();
const { scrollY, innerHeight } = window; const { scrollY, innerHeight } = window;
setStack(offsetTop > scrollY + innerHeight - height); setStack(offsetTop > scrollY + innerHeight - height);
}, [ref]); }, [ref]);
useEffect(() => { useEffect(() => {
getPlace(); getPlace();
window.addEventListener('scroll', getPlace); window.addEventListener('scroll', getPlace);
window.addEventListener('resize', getPlace); window.addEventListener('resize', getPlace);
return () => { return () => {
window.removeEventListener('scroll', getPlace); window.removeEventListener('scroll', getPlace);
window.removeEventListener('resize', getPlace); window.removeEventListener('resize', getPlace);
}; };
}, [layout]); }, [layout]);
return ( return (
<div className={styles.place} ref={ref}> <div className={styles.place} ref={ref}>
{stack ? ( {stack ? (
createPortal( createPortal(
<NodePanelInner
node={node}
stack
onEdit={onEdit}
onLike={onLike}
onStar={onStar}
can_edit={can_edit}
can_like={can_like}
can_star={can_star}
/>,
document.body
)
) : (
<NodePanelInner <NodePanelInner
node={node} node={node}
stack
onEdit={onEdit} onEdit={onEdit}
onLike={onLike} onLike={onLike}
onStar={onStar}
can_edit={can_edit} can_edit={can_edit}
can_like={can_like} can_like={can_like}
/>, can_star={can_star}
document.body />
) )}
) : ( </div>
<NodePanelInner );
node={node} }
onEdit={onEdit} );
onLike={onLike}
can_edit={can_edit}
can_like={can_like}
/>
)}
</div>
);
});
export { NodePanel }; export { NodePanel };

View file

@ -7,20 +7,24 @@ import { INode } from '~/redux/types';
import classNames from 'classnames'; import classNames from 'classnames';
interface IProps { interface IProps {
node: INode; node: Partial<INode>;
stack?: boolean; stack?: boolean;
can_edit: boolean; can_edit: boolean;
can_like: boolean; can_like: boolean;
can_star: boolean;
onEdit: () => void; onEdit: () => void;
onLike: () => void; onLike: () => void;
onStar: () => void;
} }
const NodePanelInner: FC<IProps> = ({ const NodePanelInner: FC<IProps> = ({
node: { title, user, is_liked }, node: { title, user, is_liked, is_heroic },
stack, stack,
can_star,
can_edit, can_edit,
can_like, can_like,
onStar,
onEdit, onEdit,
onLike, onLike,
}) => { }) => {
@ -35,6 +39,15 @@ const NodePanelInner: FC<IProps> = ({
</Group> </Group>
<div className={styles.buttons}> <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 && ( {can_edit && (
<div> <div>
<Icon icon="edit" size={24} onClick={onEdit} /> <Icon icon="edit" size={24} onClick={onEdit} />

View file

@ -166,3 +166,18 @@
animation: pulse 0.75s infinite; animation: pulse 0.75s infinite;
} }
} }
.star {
transition: fill, stroke 0.25s;
will-change: transform;
&:global(.is_heroic) {
svg {
fill: $orange;
}
}
&:hover {
fill: $orange;
}
}

View file

@ -15,5 +15,6 @@ export const API = {
COMMENT: (id: INode['id']) => `/node/${id}/comment`, COMMENT: (id: INode['id']) => `/node/${id}/comment`,
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`, UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
POST_LIKE: (id: INode['id']) => `/node/${id}/like`, POST_LIKE: (id: INode['id']) => `/node/${id}/like`,
POST_STAR: (id: INode['id']) => `/node/${id}/heroic`,
}, },
}; };

View file

@ -1,7 +1,7 @@
import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react'; import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { connect } from 'react-redux'; 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 { selectNode } from '~/redux/node/selectors';
import { Card } from '~/components/containers/Card'; 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 * as NODE_ACTIONS from '~/redux/node/actions';
import { CommentForm } from '~/components/node/CommentForm'; import { CommentForm } from '~/components/node/CommentForm';
import { selectUser } from '~/redux/auth/selectors'; import { selectUser } from '~/redux/auth/selectors';
import pick from 'ramda/es/pick';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
node: selectNode(state), node: selectNode(state),
@ -29,6 +30,7 @@ const mapDispatchToProps = {
nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage, nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage,
nodeEdit: NODE_ACTIONS.nodeEdit, nodeEdit: NODE_ACTIONS.nodeEdit,
nodeLike: NODE_ACTIONS.nodeLike, nodeLike: NODE_ACTIONS.nodeLike,
nodeStar: NODE_ACTIONS.nodeStar,
}; };
type IProps = ReturnType<typeof mapStateToProps> & type IProps = ReturnType<typeof mapStateToProps> &
@ -40,13 +42,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
match: { match: {
params: { id }, 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,
user: { is_user }, user: { is_user },
nodeLoadNode, nodeLoadNode,
nodeUpdateTags, nodeUpdateTags,
nodeEdit, nodeEdit,
nodeLike, nodeLike,
nodeStar,
nodeSetCoverImage, nodeSetCoverImage,
}) => { }) => {
const [layout, setLayout] = useState({}); const [layout, setLayout] = useState({});
@ -67,12 +70,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
const can_edit = useMemo(() => canEditNode(node, user), [node, user]); const can_edit = useMemo(() => canEditNode(node, user), [node, user]);
const can_like = useMemo(() => canLikeNode(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 block = node && node.type && NODE_COMPONENTS[node.type];
const inline_block = node && node.type && NODE_INLINES[node.type]; const inline_block = node && node.type && NODE_INLINES[node.type];
const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]); const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]);
const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]); const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]);
const onStar = useCallback(() => nodeStar(node.id), [nodeStar, node]);
useEffect(() => { useEffect(() => {
if (!node.cover) return; if (!node.cover) return;
@ -85,12 +90,14 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
{block && createElement(block, { node, is_loading, updateLayout, layout })} {block && createElement(block, { node, is_loading, updateLayout, layout })}
<NodePanel <NodePanel
node={node} node={pick(['title', 'user', 'is_liked', 'is_heroic'], node)}
layout={layout} layout={layout}
can_edit={can_edit} can_edit={can_edit}
can_like={can_like} can_like={can_like}
can_star={can_star}
onEdit={onEdit} onEdit={onEdit}
onLike={onLike} onLike={onLike}
onStar={onStar}
/> />
<Group> <Group>

View file

@ -80,6 +80,11 @@ export const nodeLike = (id: INode['id']) => ({
id, id,
}); });
export const nodeStar = (id: INode['id']) => ({
type: NODE_ACTIONS.STAR,
id,
});
export const nodeSetEditor = (editor: INode) => ({ export const nodeSetEditor = (editor: INode) => ({
type: NODE_ACTIONS.SET_EDITOR, type: NODE_ACTIONS.SET_EDITOR,
editor, editor,

View file

@ -1,7 +1,7 @@
import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api'; import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api';
import { INode, IResultWithStatus, IComment } from '../types'; import { INode, IResultWithStatus, IComment } from '../types';
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { nodeUpdateTags } from './actions'; import { nodeUpdateTags, nodeLike, nodeStar } from './actions';
export const postNode = ({ export const postNode = ({
access, access,
@ -79,10 +79,21 @@ export const updateNodeTags = ({
export const postNodeLike = ({ export const postNodeLike = ({
id, id,
access, access,
}: ReturnType<typeof nodeUpdateTags> & { access: string }): Promise< }: ReturnType<typeof nodeLike> & { access: string }): Promise<
IResultWithStatus<{ is_liked: INode['is_liked'] }> IResultWithStatus<{ is_liked: INode['is_liked'] }>
> => > =>
api api
.post(API.NODE.POST_LIKE(id), {}, configWithToken(access)) .post(API.NODE.POST_LIKE(id), {}, configWithToken(access))
.then(resultMiddleware) .then(resultMiddleware)
.catch(errorMiddleware); .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);

View file

@ -21,6 +21,7 @@ export const NODE_ACTIONS = {
EDIT: `${prefix}EDIT`, EDIT: `${prefix}EDIT`,
LIKE: `${prefix}LIKE`, LIKE: `${prefix}LIKE`,
STAR: `${prefix}STAR`,
CREATE: `${prefix}CREATE`, CREATE: `${prefix}CREATE`,
SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`,

View file

@ -27,6 +27,7 @@ import {
getNodeComments, getNodeComments,
updateNodeTags, updateNodeTags,
postNodeLike, postNodeLike,
postNodeStar,
} from './api'; } from './api';
import { reqWrapper } from '../auth/sagas'; import { reqWrapper } from '../auth/sagas';
import { flowSetNodes } from '../flow/actions'; import { flowSetNodes } from '../flow/actions';
@ -197,6 +198,21 @@ function* onLikeSaga({ id }: ReturnType<typeof nodeLike>) {
yield call(updateNodeEverythere, { ...current, is_liked }); 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() { export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave); yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave);
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); 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.CREATE, onCreateSaga);
yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga); yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga);
yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga); yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga);
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
} }

View file

@ -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" /> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
</g> </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"> <g id="heart" stroke="none">
<path fill="none" d="M0 0h24v24H0V0z" /> <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" /> <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" />

View file

@ -9,3 +9,6 @@ export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean
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 =>
path(['role'], user) && path(['role'], user) === USER_ROLES.ADMIN;