Add basic version of Comments component

This commit is contained in:
riggraz
2019-09-17 11:33:18 +02:00
parent d05202a2d7
commit b40ddfd543
17 changed files with 389 additions and 10 deletions

View File

@@ -1,11 +1,36 @@
class CommentsController < ApplicationController class CommentsController < ApplicationController
before_action :authenticate_user!, only: [:create]
def index def index
comments = Comment comments = Comment
.where(post_id: params[:post_id]) .where(post_id: params[:post_id])
.left_outer_joins(:user) .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) .order(updated_at: :desc)
.page(params[:page])
render json: comments render json: comments
end 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 end

View File

@@ -3,7 +3,7 @@ import { ThunkAction } from 'redux-thunk';
import { State } from '../reducers/rootReducer'; import { State } from '../reducers/rootReducer';
export const CHANGE_POST_STATUS_SUCCESS = 'CHANGE_POST_STATUS_SUCCESS'; export const CHANGE_POST_STATUS_SUCCESS = 'CHANGE_POST_STATUS_SUCCESS';
interface ChangePostStatusSuccessAction { export interface ChangePostStatusSuccessAction {
type: typeof CHANGE_POST_STATUS_SUCCESS; type: typeof CHANGE_POST_STATUS_SUCCESS;
newPostStatusId; newPostStatusId;
} }

View File

@@ -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,
});

View File

@@ -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<ICommentJSON>;
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<ICommentJSON>,
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<void, State, null, Action<string>> => 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));
}
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import IComment from '../../interfaces/IComment';
interface Props {
postId: number;
comments: Array<IComment>;
areLoading: boolean;
error: string;
page: number;
haveMore: boolean;
requestComments(postId: number, page?: number);
}
class CommentsP extends React.Component<Props> {
componentDidMount() {
this.props.requestComments(this.props.postId);
}
render() {
const {
comments,
areLoading,
error,
page,
haveMore,
} = this.props;
return (
<div>
{comments.map((comment, i) => (
<div key={i}>{comment.body}</div>
))}
</div>
);
}
}
export default CommentsP;

View File

@@ -5,6 +5,7 @@ import IPostStatus from '../../interfaces/IPostStatus';
import PostStatusSelect from './PostStatusSelect'; import PostStatusSelect from './PostStatusSelect';
import PostStatusLabel from '../shared/PostStatusLabel'; import PostStatusLabel from '../shared/PostStatusLabel';
import Comments from '../../containers/Comments';
interface Props { interface Props {
postId: number; postId: number;
@@ -15,6 +16,7 @@ interface Props {
authenticityToken: string; authenticityToken: string;
requestPost(postId: number): void; requestPost(postId: number): void;
requestComments(postId: number, page?: number): void;
changePostStatus( changePostStatus(
postId: number, postId: number,
newPostStatusId: number, newPostStatusId: number,
@@ -57,6 +59,8 @@ class PostP extends React.Component<Props> {
} }
<p>{post.description}</p> <p>{post.description}</p>
<Comments postId={this.props.postId} />
</div> </div>
); );
} }

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { requestPost } from '../actions/requestPost'; import { requestPost } from '../actions/requestPost';
import { requestComments } from '../actions/requestComments';
import { changePostStatus } from '../actions/changePostStatus'; import { changePostStatus } from '../actions/changePostStatus';
import { State } from '../reducers/rootReducer'; import { State } from '../reducers/rootReducer';
@@ -8,7 +9,8 @@ import { State } from '../reducers/rootReducer';
import PostP from '../components/Post/PostP'; import PostP from '../components/Post/PostP';
const mapStateToProps = (state: State) => ({ const mapStateToProps = (state: State) => ({
post: state.currentPost, post: state.currentPost.item,
comments: state.currentPost.comments,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@@ -16,6 +18,10 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(requestPost(postId)); dispatch(requestPost(postId));
}, },
requestComments(postId: number, page: number = 1) {
dispatch(requestComments(postId, page));
},
changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) { changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) {
if (isNaN(newPostStatusId)) newPostStatusId = null; if (isNaN(newPostStatusId)) newPostStatusId = null;

View File

@@ -0,0 +1,8 @@
interface IComment {
id: number;
body: string;
userFullName: string;
updatedAt: string;
}
export default IComment;

View File

@@ -0,0 +1,8 @@
interface ICommentJSON {
id: number;
body: string;
user_full_name: string;
updated_at: string;
}
export default ICommentJSON;

View File

@@ -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;

View File

@@ -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<IComment>;
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;

View File

@@ -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;

View File

@@ -1,7 +1,5 @@
import { import {
POST_REQUEST_START,
POST_REQUEST_SUCCESS, POST_REQUEST_SUCCESS,
POST_REQUEST_FAILURE,
} from '../actions/requestPost'; } from '../actions/requestPost';
import { import {
@@ -42,8 +40,6 @@ const postReducer = (
postStatusId: action.newPostStatusId, postStatusId: action.newPostStatusId,
}; };
case POST_REQUEST_START:
case POST_REQUEST_FAILURE:
default: default:
return state; return state;
} }

View File

@@ -2,12 +2,12 @@ import { combineReducers } from 'redux';
import postsReducer from './postsReducer'; import postsReducer from './postsReducer';
import postStatusesReducer from './postStatusesReducer'; import postStatusesReducer from './postStatusesReducer';
import postReducer from './postReducer'; import currentPostReducer from './currentPostReducer';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
posts: postsReducer, posts: postsReducer,
postStatuses: postStatusesReducer, postStatuses: postStatusesReducer,
currentPost: postReducer, currentPost: currentPostReducer,
}); });
export type State = ReturnType<typeof rootReducer> export type State = ReturnType<typeof rootReducer>

View File

@@ -5,4 +5,6 @@ class Comment < ApplicationRecord
has_many :children, class_name: 'Comment', foreign_key: 'parent_id', dependent: :destroy has_many :children, class_name: 'Comment', foreign_key: 'parent_id', dependent: :destroy
validates :body, presence: true, length: { minimum: 4 } validates :body, presence: true, length: { minimum: 4 }
paginates_per 15
end end

View File

@@ -14,7 +14,7 @@ Rails.application.routes.draw do
resources :boards, only: [:show] resources :boards, only: [:show]
resources :posts, only: [:index, :create, :show, :update] do resources :posts, only: [:index, :create, :show, :update] do
resources :comments, only: [:index] resources :comments, only: [:index, :create]
end end
resources :post_statuses, only: [:index] resources :post_statuses, only: [:index]
end end