New comments can be created

This commit is contained in:
riggraz
2019-09-18 13:40:00 +02:00
parent ecfdc54100
commit 7701c8f5e6
12 changed files with 267 additions and 18 deletions

View File

@@ -15,9 +15,11 @@ class CommentsController < ApplicationController
comment = Comment.new(comment_params) comment = Comment.new(comment_params)
if comment.save if comment.save
render json: comment, status: :no_content render json: comment, status: :created
else else
render json: I18n.t('errors.unauthorized'), status: :unauthorized render json: {
error: I18n.t('errors.comment.create', message: comment.errors.full_messages)
}, status: :unprocessable_entity
end end
end end
@@ -26,7 +28,7 @@ class CommentsController < ApplicationController
def comment_params def comment_params
params params
.require(:comment) .require(:comment)
.permit(:body) .permit(:body, :parent_id)
.merge( .merge(
user_id: current_user.id, user_id: current_user.id,
post_id: params[:post_id] post_id: params[:post_id]

View File

@@ -0,0 +1,82 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { State } from '../reducers/rootReducer';
import ICommentJSON from '../interfaces/json/IComment';
export const COMMENT_SUBMIT_START = 'COMMENT_SUBMIT_START';
interface CommentSubmitStartAction {
type: typeof COMMENT_SUBMIT_START;
parentId: number;
}
export const COMMENT_SUBMIT_SUCCESS = 'COMMENT_SUBMIT_SUCCESS';
interface CommentSubmitSuccessAction {
type: typeof COMMENT_SUBMIT_SUCCESS;
comment: ICommentJSON;
}
export const COMMENT_SUBMIT_FAILURE = 'COMMENT_SUBMIT_FAILURE';
interface CommentSubmitFailureAction {
type: typeof COMMENT_SUBMIT_FAILURE;
parentId: number;
error: string;
}
export type CommentSubmitActionTypes =
CommentSubmitStartAction |
CommentSubmitSuccessAction |
CommentSubmitFailureAction;
const commentSubmitStart = (parentId): CommentSubmitStartAction => ({
type: COMMENT_SUBMIT_START,
parentId,
});
const commentSubmitSuccess = (
commentJSON: ICommentJSON,
): CommentSubmitSuccessAction => ({
type: COMMENT_SUBMIT_SUCCESS,
comment: commentJSON,
});
const commentSubmitFailure = (parentId, error): CommentSubmitFailureAction => ({
type: COMMENT_SUBMIT_FAILURE,
parentId,
error,
});
export const submitComment = (
postId,
body,
parentId,
authenticityToken,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(commentSubmitStart(parentId));
try {
const res = await fetch(`/posts/${postId}/comments`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': authenticityToken,
},
body: JSON.stringify({
comment: {
body,
parent_id: parentId,
},
}),
});
const json = await res.json();
if (res.status === 201) {
dispatch(commentSubmitSuccess(json));
} else {
dispatch(commentSubmitFailure(parentId, json.error));
}
} catch (e) {
dispatch(commentSubmitFailure(parentId, e));
}
}

View File

@@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { FormEvent } from 'react'; import { FormEvent } from 'react';
import NewComment from './NewComment';
import { MutedText } from '../shared/CustomTexts'; import { MutedText } from '../shared/CustomTexts';
import { CommentRepliesState } from '../../reducers/commentRepliesReducer'; import { CommentRepliesState } from '../../reducers/commentRepliesReducer';
@@ -16,6 +17,7 @@ interface Props {
reply: CommentRepliesState; reply: CommentRepliesState;
handleToggleCommentReply(): void; handleToggleCommentReply(): void;
handleCommentReplyBodyChange(e: FormEvent): void; handleCommentReplyBodyChange(e: FormEvent): void;
handleSubmitComment(body: string, parentId: number): void;
} }
const Comment = ({ const Comment = ({
@@ -29,6 +31,7 @@ const Comment = ({
reply, reply,
handleToggleCommentReply, handleToggleCommentReply,
handleCommentReplyBodyChange, handleCommentReplyBodyChange,
handleSubmitComment,
}: Props) => ( }: Props) => (
<div className="comment"> <div className="comment">
<div className="commentHeader"> <div className="commentHeader">
@@ -41,9 +44,11 @@ const Comment = ({
</div> </div>
{ {
reply.isOpen ? reply.isOpen ?
<textarea <NewComment
value={reply.body} body={reply.body}
onChange={handleCommentReplyBodyChange} parentId={id}
handleChange={handleCommentReplyBodyChange}
handleSubmit={handleSubmitComment}
/> />
: :
null null

View File

@@ -12,8 +12,9 @@ interface Props {
parentId: number; parentId: number;
level: number; level: number;
toggleCommentReply(commentId: number); toggleCommentReply(commentId: number): void;
setCommentReplyBody(commentId: number, body: string); setCommentReplyBody(commentId: number, body: string): void;
handleSubmitComment(body: string, parentId: number): void;
} }
const CommentList = ({ const CommentList = ({
@@ -24,12 +25,13 @@ const CommentList = ({
toggleCommentReply, toggleCommentReply,
setCommentReplyBody, setCommentReplyBody,
handleSubmitComment,
}: Props) => ( }: Props) => (
<React.Fragment> <React.Fragment>
{comments.map((comment, i) => { {comments.map((comment, i) => {
if (comment.parentId === parentId) { if (comment.parentId === parentId) {
return ( return (
<div className="commentList"> <div className="commentList" key={i}>
<Comment <Comment
level={level} level={level}
reply={replies.find(reply => reply.commentId === comment.id)} reply={replies.find(reply => reply.commentId === comment.id)}
@@ -39,6 +41,7 @@ const CommentList = ({
setCommentReplyBody(comment.id, (e.target as HTMLTextAreaElement).value) setCommentReplyBody(comment.id, (e.target as HTMLTextAreaElement).value)
) )
} }
handleSubmitComment={handleSubmitComment}
{...comment} {...comment}
/> />
@@ -50,6 +53,7 @@ const CommentList = ({
toggleCommentReply={toggleCommentReply} toggleCommentReply={toggleCommentReply}
setCommentReplyBody={setCommentReplyBody} setCommentReplyBody={setCommentReplyBody}
handleSubmitComment={handleSubmitComment}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { FormEvent } from 'react';
import NewComment from './NewComment';
import CommentList from './CommentList'; import CommentList from './CommentList';
import Spinner from '../shared/Spinner'; import Spinner from '../shared/Spinner';
import { DangerText } from '../shared/CustomTexts'; import { DangerText } from '../shared/CustomTexts';
@@ -9,15 +11,22 @@ import { CommentRepliesState } from '../../reducers/commentRepliesReducer';
interface Props { interface Props {
postId: number; postId: number;
authenticityToken: string;
comments: Array<IComment>; comments: Array<IComment>;
replies: Array<CommentRepliesState>; replies: Array<CommentRepliesState>;
areLoading: boolean; areLoading: boolean;
error: string; error: string;
requestComments(postId: number, page?: number); requestComments(postId: number, page?: number): void;
toggleCommentReply(commentId: number); toggleCommentReply(commentId: number): void;
setCommentReplyBody(commentId: number, body: string); setCommentReplyBody(commentId: number, body: string): void;
submitComment(
postId: number,
body: string,
parentId: number,
authenticityToken: string,
): void;
} }
class CommentsP extends React.Component<Props> { class CommentsP extends React.Component<Props> {
@@ -25,6 +34,15 @@ class CommentsP extends React.Component<Props> {
this.props.requestComments(this.props.postId); this.props.requestComments(this.props.postId);
} }
_handleSubmitComment = (body, parentId) => {
this.props.submitComment(
this.props.postId,
body,
parentId,
this.props.authenticityToken,
);
}
render() { render() {
const { const {
comments, comments,
@@ -34,12 +52,24 @@ class CommentsP extends React.Component<Props> {
toggleCommentReply, toggleCommentReply,
setCommentReplyBody, setCommentReplyBody,
submitComment,
} = this.props; } = this.props;
return ( return (
<div className="comments"> <div className="comments">
<h2>Comments</h2> <h2>Comments</h2>
<NewComment
body={replies.find(reply => reply.commentId === -1) && replies.find(reply => reply.commentId === -1).body}
parentId={null}
handleChange={
(e: FormEvent) => (
setCommentReplyBody(-1, (e.target as HTMLTextAreaElement).value)
)
}
handleSubmit={this._handleSubmitComment}
/>
{ areLoading ? <Spinner /> : null } { areLoading ? <Spinner /> : null }
{ error ? <DangerText>{error}</DangerText> : null } { error ? <DangerText>{error}</DangerText> : null }
@@ -48,6 +78,7 @@ class CommentsP extends React.Component<Props> {
replies={replies} replies={replies}
toggleCommentReply={toggleCommentReply} toggleCommentReply={toggleCommentReply}
setCommentReplyBody={setCommentReplyBody} setCommentReplyBody={setCommentReplyBody}
handleSubmitComment={this._handleSubmitComment}
parentId={null} parentId={null}
level={1} level={1}
/> />

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { FormEvent } from 'react';
import Button from '../shared/Button';
interface Props {
body: string;
parentId: number;
handleChange(e: FormEvent): void;
handleSubmit(body: string, parentId: number): void;
}
const NewComment = ({
body,
parentId,
handleChange,
handleSubmit,
}: Props) => (
<div className="newCommentForm">
<textarea
value={body}
onChange={handleChange}
/>
<Button onClick={() => handleSubmit(body, parentId)}>Submit</Button>
</div>
);
export default NewComment;

View File

@@ -59,7 +59,10 @@ class PostP extends React.Component<Props> {
<p>{post.description}</p> <p>{post.description}</p>
<Comments postId={this.props.postId} /> <Comments
postId={this.props.postId}
authenticityToken={authenticityToken}
/>
</div> </div>
); );
} }

View File

@@ -5,6 +5,7 @@ import {
toggleCommentReply, toggleCommentReply,
setCommentReplyBody, setCommentReplyBody,
} from '../actions/handleCommentReplies'; } from '../actions/handleCommentReplies';
import { submitComment } from '../actions/submitComment';
import { State } from '../reducers/rootReducer'; import { State } from '../reducers/rootReducer';
@@ -29,6 +30,15 @@ const mapDispatchToProps = (dispatch) => ({
setCommentReplyBody(commentId: number, body: string) { setCommentReplyBody(commentId: number, body: string) {
dispatch(setCommentReplyBody(commentId, body)); dispatch(setCommentReplyBody(commentId, body));
}, },
submitComment(
postId: number,
body: string,
parentId: number,
authenticityToken: string,
) {
dispatch(submitComment(postId, body, parentId, authenticityToken));
},
}); });
export default connect( export default connect(

View File

@@ -1,22 +1,33 @@
import { import {
COMMENT_REQUEST_SUCCESS, COMMENT_REQUEST_SUCCESS,
} from '../actions/requestComment'; } from '../actions/requestComment';
import { import {
HandleCommentRepliesType, HandleCommentRepliesType,
TOGGLE_COMMENT_REPLY, TOGGLE_COMMENT_REPLY,
SET_COMMENT_REPLY_BODY, SET_COMMENT_REPLY_BODY,
} from '../actions/handleCommentReplies'; } from '../actions/handleCommentReplies';
import {
COMMENT_SUBMIT_START,
COMMENT_SUBMIT_SUCCESS,
COMMENT_SUBMIT_FAILURE,
} from '../actions/submitComment';
export interface CommentRepliesState { export interface CommentRepliesState {
commentId: number; commentId: number;
isOpen: boolean; isOpen: boolean;
body: string; body: string;
isSubmitting: boolean;
error: string;
} }
const initialState: CommentRepliesState = { const initialState: CommentRepliesState = {
commentId: undefined, commentId: undefined,
isOpen: false, isOpen: false,
body: '', body: '',
isSubmitting: false,
error: '',
} }
const commentRepliesReducer = ( const commentRepliesReducer = (
@@ -42,6 +53,27 @@ const commentRepliesReducer = (
body: action.body, body: action.body,
}; };
case COMMENT_SUBMIT_START:
return {
...state,
isSubmitting: true,
};
case COMMENT_SUBMIT_SUCCESS:
return {
...state,
isOpen: false,
body: '',
isSubmitting: false,
error: '',
};
case COMMENT_SUBMIT_FAILURE:
return {
...state,
error: action.error,
};
default: default:
return state; return state;
} }

View File

@@ -13,6 +13,12 @@ import {
SET_COMMENT_REPLY_BODY, SET_COMMENT_REPLY_BODY,
} from '../actions/handleCommentReplies'; } from '../actions/handleCommentReplies';
import {
COMMENT_SUBMIT_START,
COMMENT_SUBMIT_SUCCESS,
COMMENT_SUBMIT_FAILURE,
} from '../actions/submitComment';
import commentReducer from './commentReducer'; import commentReducer from './commentReducer';
import commentRepliesReducer from './commentRepliesReducer'; import commentRepliesReducer from './commentRepliesReducer';
@@ -50,9 +56,10 @@ const commentsReducer = (
items: action.comments.map( items: action.comments.map(
comment => commentReducer(undefined, commentRequestSuccess(comment)) comment => commentReducer(undefined, commentRequestSuccess(comment))
), ),
replies: action.comments.map( replies: [commentRepliesReducer(undefined, {type: 'COMMENT_REQUEST_SUCCESS', comment: { id: -1 } }),
...action.comments.map(
comment => commentRepliesReducer(undefined, commentRequestSuccess(comment)) comment => commentRepliesReducer(undefined, commentRequestSuccess(comment))
), )],
areLoading: false, areLoading: false,
error: '', error: '',
}; };
@@ -78,6 +85,38 @@ const commentsReducer = (
), ),
}; };
case COMMENT_SUBMIT_START:
case COMMENT_SUBMIT_FAILURE:
return {
...state,
replies: state.replies.map(
reply => (
reply.commentId === action.parentId ?
commentRepliesReducer(reply, action)
:
reply
)
),
};
case COMMENT_SUBMIT_SUCCESS:
console.log(action.comment);
return {
...state,
items: [commentReducer(undefined, commentRequestSuccess(action.comment)), ...state.items],
replies: [
...state.replies.map(
reply => (
reply.commentId === action.comment.parent_id ?
commentRepliesReducer(reply, action)
:
reply
)
),
commentRepliesReducer(undefined, commentRequestSuccess(action.comment)),
],
};
default: default:
return state; return state;
} }

View File

@@ -23,6 +23,13 @@ import {
SET_COMMENT_REPLY_BODY, SET_COMMENT_REPLY_BODY,
} from '../actions/handleCommentReplies'; } from '../actions/handleCommentReplies';
import {
CommentSubmitActionTypes,
COMMENT_SUBMIT_START,
COMMENT_SUBMIT_SUCCESS,
COMMENT_SUBMIT_FAILURE,
} from '../actions/submitComment';
import postReducer from './postReducer'; import postReducer from './postReducer';
import commentsReducer from './commentsReducer'; import commentsReducer from './commentsReducer';
@@ -50,7 +57,8 @@ const currentPostReducer = (
PostRequestActionTypes | PostRequestActionTypes |
ChangePostStatusSuccessAction | ChangePostStatusSuccessAction |
CommentsRequestActionTypes | CommentsRequestActionTypes |
HandleCommentRepliesType HandleCommentRepliesType |
CommentSubmitActionTypes
): CurrentPostState => { ): CurrentPostState => {
switch (action.type) { switch (action.type) {
case POST_REQUEST_START: case POST_REQUEST_START:
@@ -85,6 +93,9 @@ const currentPostReducer = (
case COMMENTS_REQUEST_FAILURE: case COMMENTS_REQUEST_FAILURE:
case TOGGLE_COMMENT_REPLY: case TOGGLE_COMMENT_REPLY:
case SET_COMMENT_REPLY_BODY: case SET_COMMENT_REPLY_BODY:
case COMMENT_SUBMIT_START:
case COMMENT_SUBMIT_SUCCESS:
case COMMENT_SUBMIT_FAILURE:
return { return {
...state, ...state,
comments: commentsReducer(state.comments, action), comments: commentsReducer(state.comments, action),

View File

@@ -4,3 +4,5 @@ en:
post: post:
create: 'Post create error: %{message}' create: 'Post create error: %{message}'
update: 'Post update error: %{message}' update: 'Post update error: %{message}'
comment:
create: 'Comment create error: %{message}'