From b40ddfd543343f505171d6b1f47f20ac9b1a44f2 Mon Sep 17 00:00:00 2001 From: riggraz Date: Tue, 17 Sep 2019 11:33:18 +0200 Subject: [PATCH] Add basic version of Comments component --- app/controllers/comments_controller.rb | 27 +++++- app/javascript/actions/changePostStatus.ts | 2 +- app/javascript/actions/requestComment.ts | 12 +++ app/javascript/actions/requestComments.ts | 63 ++++++++++++++ .../components/Comments/CommentsP.tsx | 41 +++++++++ app/javascript/components/Post/PostP.tsx | 4 + app/javascript/containers/Comments.tsx | 26 ++++++ app/javascript/containers/Post.tsx | 8 +- app/javascript/interfaces/IComment.ts | 8 ++ app/javascript/interfaces/json/IComment.ts | 8 ++ app/javascript/reducers/commentReducer.ts | 32 +++++++ app/javascript/reducers/commentsReducer.ts | 70 +++++++++++++++ app/javascript/reducers/currentPostReducer.ts | 86 +++++++++++++++++++ app/javascript/reducers/postReducer.ts | 4 - app/javascript/reducers/rootReducer.ts | 4 +- app/models/comment.rb | 2 + config/routes.rb | 2 +- 17 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 app/javascript/actions/requestComment.ts create mode 100644 app/javascript/actions/requestComments.ts create mode 100644 app/javascript/components/Comments/CommentsP.tsx create mode 100644 app/javascript/containers/Comments.tsx create mode 100644 app/javascript/interfaces/IComment.ts create mode 100644 app/javascript/interfaces/json/IComment.ts create mode 100644 app/javascript/reducers/commentReducer.ts create mode 100644 app/javascript/reducers/commentsReducer.ts create mode 100644 app/javascript/reducers/currentPostReducer.ts diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 80a6b322..37bc3d19 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,11 +1,36 @@ class CommentsController < ApplicationController + before_action :authenticate_user!, only: [:create] + def index comments = Comment .where(post_id: params[:post_id]) .left_outer_joins(:user) - .select('comments.body, comments.updated_at, users.full_name') + .select('comments.id, comments.body, comments.updated_at, users.full_name as user_full_name') .order(updated_at: :desc) + .page(params[:page]) render json: comments end + + def create + comment = Comment.new(comment_params) + + if comment.save + render json: comment, status: :no_content + else + render json: I18n.t('errors.unauthorized'), status: :unauthorized + end + end + + private + + def comment_params + params + .require(:comment) + .permit(:body) + .merge( + user_id: current_user.id, + post_id: params[:post_id] + ) + end end diff --git a/app/javascript/actions/changePostStatus.ts b/app/javascript/actions/changePostStatus.ts index fdeb095c..29c915d9 100644 --- a/app/javascript/actions/changePostStatus.ts +++ b/app/javascript/actions/changePostStatus.ts @@ -3,7 +3,7 @@ import { ThunkAction } from 'redux-thunk'; import { State } from '../reducers/rootReducer'; export const CHANGE_POST_STATUS_SUCCESS = 'CHANGE_POST_STATUS_SUCCESS'; -interface ChangePostStatusSuccessAction { +export interface ChangePostStatusSuccessAction { type: typeof CHANGE_POST_STATUS_SUCCESS; newPostStatusId; } diff --git a/app/javascript/actions/requestComment.ts b/app/javascript/actions/requestComment.ts new file mode 100644 index 00000000..9f3f67c6 --- /dev/null +++ b/app/javascript/actions/requestComment.ts @@ -0,0 +1,12 @@ +import ICommentJSON from '../interfaces/json/IComment'; + +export const COMMENT_REQUEST_SUCCESS = 'COMMENT_REQUEST_SUCCESS'; +interface CommentRequestSuccessAction { + type: typeof COMMENT_REQUEST_SUCCESS; + comment: ICommentJSON; +} + +export const commentRequestSuccess = (comment: ICommentJSON): CommentRequestSuccessAction => ({ + type: COMMENT_REQUEST_SUCCESS, + comment, +}); \ No newline at end of file diff --git a/app/javascript/actions/requestComments.ts b/app/javascript/actions/requestComments.ts new file mode 100644 index 00000000..74e93c79 --- /dev/null +++ b/app/javascript/actions/requestComments.ts @@ -0,0 +1,63 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import ICommentJSON from '../interfaces/json/IComment'; + +import { State } from '../reducers/rootReducer'; + +export const COMMENTS_REQUEST_START = 'COMMENTS_REQUEST_START'; +interface CommentsRequestStartAction { + type: typeof COMMENTS_REQUEST_START; +} + +export const COMMENTS_REQUEST_SUCCESS = 'COMMENTS_REQUEST_SUCCESS'; +interface CommentsRequestSuccessAction { + type: typeof COMMENTS_REQUEST_SUCCESS; + comments: Array; + page: number; +} + +export const COMMENTS_REQUEST_FAILURE = 'COMMENTS_REQUEST_FAILURE'; +interface CommentsRequestFailureAction { + type: typeof COMMENTS_REQUEST_FAILURE; + error: string; +} + +export type CommentsRequestActionTypes = + CommentsRequestStartAction | + CommentsRequestSuccessAction | + CommentsRequestFailureAction; + + +const commentsRequestStart = (): CommentsRequestActionTypes => ({ + type: COMMENTS_REQUEST_START, +}); + +const commentsRequestSuccess = ( + comments: Array, + page: number +): CommentsRequestActionTypes => ({ + type: COMMENTS_REQUEST_SUCCESS, + comments, + page, +}); + +const commentsRequestFailure = (error: string): CommentsRequestActionTypes => ({ + type: COMMENTS_REQUEST_FAILURE, + error, +}); + +export const requestComments = ( + postId: number, + page: number, +): ThunkAction> => async (dispatch) => { + dispatch(commentsRequestStart()); + + try { + const response = await fetch(`/posts/${postId}/comments?page=${page}`); + const json = await response.json(); + dispatch(commentsRequestSuccess(json, page)); + } catch (e) { + dispatch(commentsRequestFailure(e)); + } +} \ No newline at end of file diff --git a/app/javascript/components/Comments/CommentsP.tsx b/app/javascript/components/Comments/CommentsP.tsx new file mode 100644 index 00000000..c10de968 --- /dev/null +++ b/app/javascript/components/Comments/CommentsP.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; + +import IComment from '../../interfaces/IComment'; + +interface Props { + postId: number; + + comments: Array; + areLoading: boolean; + error: string; + page: number; + haveMore: boolean; + + requestComments(postId: number, page?: number); +} + +class CommentsP extends React.Component { + componentDidMount() { + this.props.requestComments(this.props.postId); + } + + render() { + const { + comments, + areLoading, + error, + page, + haveMore, + } = this.props; + + return ( +
+ {comments.map((comment, i) => ( +
{comment.body}
+ ))} +
+ ); + } +} + +export default CommentsP; \ No newline at end of file diff --git a/app/javascript/components/Post/PostP.tsx b/app/javascript/components/Post/PostP.tsx index 36ffd661..4f3fb383 100644 --- a/app/javascript/components/Post/PostP.tsx +++ b/app/javascript/components/Post/PostP.tsx @@ -5,6 +5,7 @@ import IPostStatus from '../../interfaces/IPostStatus'; import PostStatusSelect from './PostStatusSelect'; import PostStatusLabel from '../shared/PostStatusLabel'; +import Comments from '../../containers/Comments'; interface Props { postId: number; @@ -15,6 +16,7 @@ interface Props { authenticityToken: string; requestPost(postId: number): void; + requestComments(postId: number, page?: number): void; changePostStatus( postId: number, newPostStatusId: number, @@ -57,6 +59,8 @@ class PostP extends React.Component { }

{post.description}

+ + ); } diff --git a/app/javascript/containers/Comments.tsx b/app/javascript/containers/Comments.tsx new file mode 100644 index 00000000..4ac7879c --- /dev/null +++ b/app/javascript/containers/Comments.tsx @@ -0,0 +1,26 @@ +import { connect } from 'react-redux'; + +import { requestComments } from '../actions/requestComments'; + +import { State } from '../reducers/rootReducer'; + +import CommentsP from '../components/Comments/CommentsP'; + +const mapStateToProps = (state: State) => ({ + comments: state.currentPost.comments.items, + areLoading: state.currentPost.comments.areLoading, + error: state.currentPost.comments.error, + page: state.currentPost.comments.page, + haveMore: state.currentPost.comments.haveMore, +}); + +const mapDispatchToProps = (dispatch) => ({ + requestComments(postId: number, page: number = 1) { + dispatch(requestComments(postId, page)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CommentsP); \ No newline at end of file diff --git a/app/javascript/containers/Post.tsx b/app/javascript/containers/Post.tsx index 9c129bef..e1ff68e5 100644 --- a/app/javascript/containers/Post.tsx +++ b/app/javascript/containers/Post.tsx @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { requestPost } from '../actions/requestPost'; +import { requestComments } from '../actions/requestComments'; import { changePostStatus } from '../actions/changePostStatus'; import { State } from '../reducers/rootReducer'; @@ -8,7 +9,8 @@ import { State } from '../reducers/rootReducer'; import PostP from '../components/Post/PostP'; const mapStateToProps = (state: State) => ({ - post: state.currentPost, + post: state.currentPost.item, + comments: state.currentPost.comments, }); const mapDispatchToProps = (dispatch) => ({ @@ -16,6 +18,10 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(requestPost(postId)); }, + requestComments(postId: number, page: number = 1) { + dispatch(requestComments(postId, page)); + }, + changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) { if (isNaN(newPostStatusId)) newPostStatusId = null; diff --git a/app/javascript/interfaces/IComment.ts b/app/javascript/interfaces/IComment.ts new file mode 100644 index 00000000..7a5d44ef --- /dev/null +++ b/app/javascript/interfaces/IComment.ts @@ -0,0 +1,8 @@ +interface IComment { + id: number; + body: string; + userFullName: string; + updatedAt: string; +} + +export default IComment; \ No newline at end of file diff --git a/app/javascript/interfaces/json/IComment.ts b/app/javascript/interfaces/json/IComment.ts new file mode 100644 index 00000000..6ade8404 --- /dev/null +++ b/app/javascript/interfaces/json/IComment.ts @@ -0,0 +1,8 @@ +interface ICommentJSON { + id: number; + body: string; + user_full_name: string; + updated_at: string; +} + +export default ICommentJSON; \ No newline at end of file diff --git a/app/javascript/reducers/commentReducer.ts b/app/javascript/reducers/commentReducer.ts new file mode 100644 index 00000000..0ad233b0 --- /dev/null +++ b/app/javascript/reducers/commentReducer.ts @@ -0,0 +1,32 @@ +import { + COMMENT_REQUEST_SUCCESS, +} from '../actions/requestComment'; + +import IComment from '../interfaces/IComment'; + +const initialState: IComment = { + id: 0, + body: '', + userFullName: '', + updatedAt: '', +}; + +const commentReducer = ( + state = initialState, + action, +): IComment => { + switch (action.type) { + case COMMENT_REQUEST_SUCCESS: + return { + id: action.comment.id, + body: action.comment.body, + userFullName: action.comment.user_full_name, + updatedAt: action.comment.updated_at, + }; + + default: + return state; + } +} + +export default commentReducer; \ No newline at end of file diff --git a/app/javascript/reducers/commentsReducer.ts b/app/javascript/reducers/commentsReducer.ts new file mode 100644 index 00000000..eda17cc1 --- /dev/null +++ b/app/javascript/reducers/commentsReducer.ts @@ -0,0 +1,70 @@ +import { + CommentsRequestActionTypes, + COMMENTS_REQUEST_START, + COMMENTS_REQUEST_SUCCESS, + COMMENTS_REQUEST_FAILURE, +} from '../actions/requestComments'; +import { commentRequestSuccess } from '../actions/requestComment'; + +import commentReducer from './commentReducer'; + +import IComment from '../interfaces/IComment'; + +export interface CommentsState { + items: Array; + areLoading: boolean; + error: string; + page: number; + haveMore: boolean; +} + +const initialState: CommentsState = { + items: [], + areLoading: false, + error: '', + page: 0, + haveMore: true, +}; + +const commentsReducer = ( + state = initialState, + action, +): CommentsState => { + switch (action.type) { + case COMMENTS_REQUEST_START: + return { + ...state, + areLoading: true, + }; + + case COMMENTS_REQUEST_SUCCESS: + return { + ...state, + items: action.page === 1 ? + action.comments.map(comment => commentReducer(undefined, commentRequestSuccess(comment))) + : + [ + ...state.items, + ...action.comments.map( + comment => commentReducer(undefined, commentRequestSuccess(comment)) + ), + ], + page: action.page, + haveMore: action.comments.length === 15, + areLoading: false, + error: '', + }; + + case COMMENTS_REQUEST_FAILURE: + return { + ...state, + areLoading: false, + error: action.error, + }; + + default: + return state; + } +} + +export default commentsReducer; \ No newline at end of file diff --git a/app/javascript/reducers/currentPostReducer.ts b/app/javascript/reducers/currentPostReducer.ts new file mode 100644 index 00000000..d87ec4ce --- /dev/null +++ b/app/javascript/reducers/currentPostReducer.ts @@ -0,0 +1,86 @@ +import { + PostRequestActionTypes, + POST_REQUEST_START, + POST_REQUEST_SUCCESS, + POST_REQUEST_FAILURE, +} from '../actions/requestPost'; + +import { + ChangePostStatusSuccessAction, + CHANGE_POST_STATUS_SUCCESS, +} from '../actions/changePostStatus'; + +import { + CommentsRequestActionTypes, + COMMENTS_REQUEST_START, + COMMENTS_REQUEST_SUCCESS, + COMMENTS_REQUEST_FAILURE, +} from '../actions/requestComments'; + +import postReducer from './postReducer'; +import commentsReducer from './commentsReducer'; + +import { CommentsState } from './commentsReducer'; + +import IPost from '../interfaces/IPost'; + +interface CurrentPostState { + item: IPost; + isLoading: boolean; + error: string; + comments: CommentsState; +} + +const initialState: CurrentPostState = { + item: postReducer(undefined, {}), + isLoading: false, + error: '', + comments: commentsReducer(undefined, {}), +}; + +const currentPostReducer = ( + state = initialState, + action: PostRequestActionTypes | ChangePostStatusSuccessAction | CommentsRequestActionTypes, +): CurrentPostState => { + switch (action.type) { + case POST_REQUEST_START: + return { + ...state, + isLoading: true, + }; + + case POST_REQUEST_SUCCESS: + return { + ...state, + item: postReducer(undefined, action), + isLoading: false, + error: '', + }; + + case POST_REQUEST_FAILURE: + return { + ...state, + isLoading: false, + error: action.error, + }; + + case CHANGE_POST_STATUS_SUCCESS: + return { + ...state, + item: postReducer(state.item, action), + }; + + case COMMENTS_REQUEST_START: + case COMMENTS_REQUEST_SUCCESS: + case COMMENTS_REQUEST_FAILURE: + return { + ...state, + comments: commentsReducer(state.comments, action), + }; + + default: + return state; + } +} + +export default currentPostReducer; \ No newline at end of file diff --git a/app/javascript/reducers/postReducer.ts b/app/javascript/reducers/postReducer.ts index bf8b2198..d0400ef2 100644 --- a/app/javascript/reducers/postReducer.ts +++ b/app/javascript/reducers/postReducer.ts @@ -1,7 +1,5 @@ import { - POST_REQUEST_START, POST_REQUEST_SUCCESS, - POST_REQUEST_FAILURE, } from '../actions/requestPost'; import { @@ -42,8 +40,6 @@ const postReducer = ( postStatusId: action.newPostStatusId, }; - case POST_REQUEST_START: - case POST_REQUEST_FAILURE: default: return state; } diff --git a/app/javascript/reducers/rootReducer.ts b/app/javascript/reducers/rootReducer.ts index 066f40c9..22e0dcae 100644 --- a/app/javascript/reducers/rootReducer.ts +++ b/app/javascript/reducers/rootReducer.ts @@ -2,12 +2,12 @@ import { combineReducers } from 'redux'; import postsReducer from './postsReducer'; import postStatusesReducer from './postStatusesReducer'; -import postReducer from './postReducer'; +import currentPostReducer from './currentPostReducer'; const rootReducer = combineReducers({ posts: postsReducer, postStatuses: postStatusesReducer, - currentPost: postReducer, + currentPost: currentPostReducer, }); export type State = ReturnType diff --git a/app/models/comment.rb b/app/models/comment.rb index d3c4f2dc..62da3d65 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,4 +5,6 @@ class Comment < ApplicationRecord has_many :children, class_name: 'Comment', foreign_key: 'parent_id', dependent: :destroy validates :body, presence: true, length: { minimum: 4 } + + paginates_per 15 end diff --git a/config/routes.rb b/config/routes.rb index a4cc7733..38a66f99 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,7 +14,7 @@ Rails.application.routes.draw do resources :boards, only: [:show] resources :posts, only: [:index, :create, :show, :update] do - resources :comments, only: [:index] + resources :comments, only: [:index, :create] end resources :post_statuses, only: [:index] end