From 76a3331719b3ae7bd15663d5f915cf29510bcb86 Mon Sep 17 00:00:00 2001 From: muerwre Date: Mon, 26 Aug 2019 15:37:11 +0700 Subject: [PATCH] adding comments --- src/components/node/Comment/index.tsx | 6 ++-- src/components/node/Comment/styles.scss | 3 ++ src/components/node/CommentForm/index.tsx | 34 ++++++++++++++------ src/components/node/NodeComments/index.tsx | 12 ++++--- src/components/node/NodeComments/styles.scss | 16 +++++++++ src/constants/api.ts | 4 +++ src/containers/node/NodeLayout/index.tsx | 4 +-- src/redux/node/actions.ts | 18 ++++++++++- src/redux/node/api.ts | 16 ++++++++- src/redux/node/constants.ts | 13 ++++++-- src/redux/node/handlers.ts | 12 +++++++ src/redux/node/reducer.ts | 4 +++ src/redux/node/sagas.ts | 26 ++++++++++++++- src/redux/types.ts | 6 ++-- 14 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 src/components/node/NodeComments/styles.scss diff --git a/src/components/node/Comment/index.tsx b/src/components/node/Comment/index.tsx index 287bd2a0..2ec98a02 100644 --- a/src/components/node/Comment/index.tsx +++ b/src/components/node/Comment/index.tsx @@ -1,16 +1,18 @@ import React, { FC, HTMLAttributes } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; +import { IComment } from '~/redux/types'; +import * as styles from './styles.scss'; type IProps = HTMLAttributes & { is_empty?: boolean; is_loading?: boolean; photo?: string; - comment?: any; + comment?: IComment; }; const Comment: FC = ({ comment, is_empty, is_loading, className, photo, ...props }) => ( -
Something!
+ {comment.text &&
{comment.text}
}
); diff --git a/src/components/node/Comment/styles.scss b/src/components/node/Comment/styles.scss index e69de29b..39eb504e 100644 --- a/src/components/node/Comment/styles.scss +++ b/src/components/node/Comment/styles.scss @@ -0,0 +1,3 @@ +.text { + padding: $gap / 2; +} diff --git a/src/components/node/CommentForm/index.tsx b/src/components/node/CommentForm/index.tsx index 13394561..b0a78080 100644 --- a/src/components/node/CommentForm/index.tsx +++ b/src/components/node/CommentForm/index.tsx @@ -1,18 +1,27 @@ -import React, { FC, useCallback, useState, ChangeEvent, ChangeEventHandler } from 'react'; +import React, { FC, useCallback, useState } from 'react'; import { Textarea } from '~/components/input/Textarea'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; import * as styles from './styles.scss'; import { Filler } from '~/components/containers/Filler'; import { Button } from '~/components/input/Button'; import assocPath from 'ramda/es/assocPath'; -import { InputHandler, INode } from '~/redux/types'; +import { InputHandler, INode, IComment } from '~/redux/types'; +import { connect } from 'react-redux'; +import * as NODE_ACTIONS from '~/redux/node/actions'; +import { EMPTY_COMMENT } from '~/redux/node/constants'; -interface IProps { - id: INode['id']; -} +const mapStateToProps = () => ({}); +const mapDispatchToProps = { + nodePostComment: NODE_ACTIONS.nodePostComment, +}; -const CommentForm: FC = ({ id }) => { - const [data, setData] = useState({ text: '' }); +type IProps = ReturnType & + typeof mapDispatchToProps & { + id: INode['id']; + }; + +const CommentFormUnconnected: FC = ({ nodePostComment, id }) => { + const [data, setData] = useState({ ...EMPTY_COMMENT }); const onInput = useCallback( text => { @@ -24,9 +33,9 @@ const CommentForm: FC = ({ id }) => { const onSubmit = useCallback( event => { event.preventDefault(); - console.log({ data }); + nodePostComment(data, id); }, - [data] + [data, nodePostComment, id] ); return ( @@ -46,4 +55,9 @@ const CommentForm: FC = ({ id }) => { ); }; -export { CommentForm }; +const CommentForm = connect( + mapStateToProps, + mapDispatchToProps +)(CommentFormUnconnected); + +export { CommentForm, CommentFormUnconnected }; diff --git a/src/components/node/NodeComments/index.tsx b/src/components/node/NodeComments/index.tsx index 52a6dc18..afc7acdd 100644 --- a/src/components/node/NodeComments/index.tsx +++ b/src/components/node/NodeComments/index.tsx @@ -4,17 +4,21 @@ import { Comment } from '../Comment'; import { INode } from '~/redux/types'; import { CommentForm } from '../CommentForm'; import { Group } from '~/components/containers/Group'; +import * as styles from './styles.scss'; +import { Filler } from '~/components/containers/Filler'; interface IProps { comments?: any; } const NodeComments: FC = ({ comments }) => ( - - {range(1, 6).map(el => ( - +
+ + + {comments.map(comment => ( + ))} - +
); export { NodeComments }; diff --git a/src/components/node/NodeComments/styles.scss b/src/components/node/NodeComments/styles.scss new file mode 100644 index 00000000..e6ccf51a --- /dev/null +++ b/src/components/node/NodeComments/styles.scss @@ -0,0 +1,16 @@ +.wrap { + display: flex; + flex-direction: column-reverse !important; + + & > div { + margin: ($gap / 2) 0; + + &:last-child { + margin-top: 0; + } + + &:first-child { + margin-bottom: 0; + } + } +} diff --git a/src/constants/api.ts b/src/constants/api.ts index 8ddb366f..7f31d612 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,3 +1,5 @@ +import { INode } from '~/redux/types'; + export const API = { BASE: process.env.API_HOST, USER: { @@ -9,5 +11,7 @@ export const API = { SAVE: '/node/', GET: '/node/', GET_NODE: (id: number | string) => `/node/${id}`, + + COMMENT: (id: INode['id']) => `/node/${id}/comment`, }, }; diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index 4919723d..b329b578 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -55,10 +55,10 @@ const NodeLayoutUnconnected: FC = ({ - {is_loading_comments || !comments.length || true ? ( + {is_loading_comments || !comments.length ? ( ) : ( - + )} diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts index 369f5abb..fefccd08 100644 --- a/src/redux/node/actions.ts +++ b/src/redux/node/actions.ts @@ -1,4 +1,4 @@ -import { INode, IValidationErrors } from '../types'; +import { INode, IValidationErrors, IComment } from '../types'; import { NODE_ACTIONS } from './constants'; import { INodeState } from './reducer'; @@ -32,3 +32,19 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({ current, type: NODE_ACTIONS.SET_CURRENT, }); + +export const nodePostComment = (data: IComment, id: INode['id']) => ({ + data, + id, + type: NODE_ACTIONS.POST_COMMENT, +}); + +export const nodeSetSendingComment = (is_sending_comment: boolean) => ({ + is_sending_comment, + type: NODE_ACTIONS.SET_SENDING_COMMENT, +}); + +export const nodeSetComments = (comments: IComment[]) => ({ + comments, + type: NODE_ACTIONS.SET_COMMENTS, +}); diff --git a/src/redux/node/api.ts b/src/redux/node/api.ts index cd798da6..be439b89 100644 --- a/src/redux/node/api.ts +++ b/src/redux/node/api.ts @@ -1,5 +1,5 @@ import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api'; -import { INode, IResultWithStatus } from '../types'; +import { INode, IResultWithStatus, IComment } from '../types'; import { API } from '~/constants/api'; export const postNode = ({ @@ -33,3 +33,17 @@ export const getNode = ({ .get(API.NODE.GET_NODE(id)) .then(resultMiddleware) .catch(errorMiddleware); + +export const postNodeComment = ({ + id, + data, + access, +}: { + access: string; + id: number; + data: IComment; +}): Promise> => + api + .post(API.NODE.COMMENT(id), data, configWithToken(access)) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 8063dda5..1589a166 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,7 +1,6 @@ import { FC } from 'react'; -import { IBlock, INode, ValueOf } from '../types'; +import { IBlock, INode, ValueOf, IComment } from '../types'; import { NodeImageBlock } from '~/components/node/NodeImageBlock'; -import { NodeImageBlockPlaceholder } from '~/components/node/NodeImageBlockPlaceholder'; const prefix = 'NODE.'; export const NODE_ACTIONS = { @@ -11,7 +10,10 @@ export const NODE_ACTIONS = { 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`, + POST_COMMENT: `${prefix}POST_COMMENT`, + SET_COMMENTS: `${prefix}SET_COMMENTS`, }; export const EMPTY_BLOCK: IBlock = { @@ -54,3 +56,10 @@ type INodeComponents = Record, FC<{ node: INode; is_l export const NODE_COMPONENTS: INodeComponents = { [NODE_TYPES.IMAGE]: NodeImageBlock, }; + +export const EMPTY_COMMENT: IComment = { + text: '', + files: [], + is_private: false, + owner: null, +}; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index 09dbbe8a..b9d24cf4 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -5,6 +5,8 @@ import { nodeSetLoading, nodeSetCurrent, nodeSetLoadingComments, + nodeSetSendingComment, + nodeSetComments, } from './actions'; import { INodeState } from './reducer'; @@ -22,9 +24,19 @@ const setLoadingComments = ( const setCurrent = (state: INodeState, { current }: ReturnType) => assocPath(['current'], current, state); +const setSendingComment = ( + state: INodeState, + { is_sending_comment }: ReturnType +) => assocPath(['is_sending_comment'], is_sending_comment, state); + +const setComments = (state: INodeState, { comments }: ReturnType) => + assocPath(['comments'], comments, state); + export const NODE_HANDLERS = { [NODE_ACTIONS.SAVE]: 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, }; diff --git a/src/redux/node/reducer.ts b/src/redux/node/reducer.ts index 01980b68..20bfbb8b 100644 --- a/src/redux/node/reducer.ts +++ b/src/redux/node/reducer.ts @@ -13,6 +13,7 @@ export type INodeState = Readonly<{ is_loading: boolean; is_loading_comments: boolean; + is_sending_comment: boolean; }>; const INITIAL_STATE: INodeState = { @@ -24,8 +25,11 @@ const INITIAL_STATE: INodeState = { }, current: { ...EMPTY_NODE }, comments: [], + is_loading: false, is_loading_comments: false, + is_sending_comment: false, + error: null, errors: {}, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index 26a10270..b700e763 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -9,14 +9,18 @@ import { nodeSetLoading, nodeSetCurrent, nodeSetLoadingComments, + nodePostComment, + nodeSetSendingComment, + nodeSetComments, } from './actions'; -import { postNode, getNode } from './api'; +import { postNode, getNode, postNodeComment } from './api'; import { reqWrapper } from '../auth/sagas'; import { flowSetNodes } from '../flow/actions'; import { ERRORS } from '~/constants/errors'; import { modalSetShown } from '../modal/actions'; import { selectFlowNodes } from '../flow/selectors'; import { URLS } from '~/constants/urls'; +import { selectNode } from './selectors'; function* onNodeSave({ node }: ReturnType) { yield put(nodeSetSaveErrors({})); @@ -67,7 +71,27 @@ function* onNodeLoad({ id, node_type }: ReturnType) { return; } +function* onPostComment({ data, id }: ReturnType) { + yield put(nodeSetSendingComment(true)); + const { + data: { comment }, + error, + } = yield call(reqWrapper, postNodeComment, { data, id }); + yield put(nodeSetSendingComment(false)); + + if (error || !comment) { + return yield put(nodeSetSaveErrors({ error: error || ERRORS.EMPTY_RESPONSE })); + } + + console.log({ comment }); + + const { comments } = yield select(selectNode); + + yield put(nodeSetComments([...comments, comment])); +} + export default function* nodeSaga() { yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave); yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); + yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment); } diff --git a/src/redux/types.ts b/src/redux/types.ts index ab643f32..eb6383a6 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -116,12 +116,12 @@ export interface INode { export interface IComment { text: string; - attaches: IFile[]; + files: IFile[]; is_private: boolean; owner: IUser; - created_at: string; - update_at: string; + created_at?: string; + update_at?: string; } export type IUploadProgressHandler = (progress: ProgressEvent) => void;