Add Likes in Post component

This commit is contained in:
riggraz
2019-09-30 16:54:37 +02:00
parent dfee92da9c
commit 84263b9d33
15 changed files with 293 additions and 19 deletions

View File

@@ -1,11 +1,28 @@
class LikesController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_user!, only: [:create, :destroy]
def index
likes = Like
.select(
:id,
:full_name,
:email
)
.left_outer_joins(:user)
.where(post_id: params[:post_id])
render json: likes
end
def create
like = Like.new(like_params)
if like.save
render json: like, status: :created
render json: {
id: like.id,
full_name: current_user.full_name,
email: current_user.email,
}, status: :created
else
render json: {
error: I18n.t('errors.likes.create', message: like.errors.full_messages)
@@ -15,11 +32,14 @@ class LikesController < ApplicationController
def destroy
like = Like.find_by(like_params)
id = like.id
return if like.nil?
if like.destroy
render json: {}, status: :accepted
render json: {
id: id,
}, status: :accepted
else
render json: {
error: I18n.t('errors.likes.destroy', message: like.errors.full_messages)

View File

@@ -0,0 +1,59 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import ILikeJSON from '../interfaces/json/ILike';
import { State } from '../reducers/rootReducer';
export const LIKES_REQUEST_START = 'LIKES_REQUEST_START';
interface LikesRequestStartAction {
type: typeof LIKES_REQUEST_START;
}
export const LIKES_REQUEST_SUCCESS = 'LIKES_REQUEST_SUCCESS';
interface LikesRequestSuccessAction {
type: typeof LIKES_REQUEST_SUCCESS;
likes: Array<ILikeJSON>;
}
export const LIKES_REQUEST_FAILURE = 'LIKES_REQUEST_FAILURE';
interface LikesRequestFailureAction {
type: typeof LIKES_REQUEST_FAILURE;
error: string;
}
export type LikesRequestActionTypes =
LikesRequestStartAction |
LikesRequestSuccessAction |
LikesRequestFailureAction;
const likesRequestStart = (): LikesRequestActionTypes => ({
type: LIKES_REQUEST_START,
});
const likesRequestSuccess = (
likes: Array<ILikeJSON>,
): LikesRequestActionTypes => ({
type: LIKES_REQUEST_SUCCESS,
likes,
});
const likesRequestFailure = (error: string): LikesRequestActionTypes => ({
type: LIKES_REQUEST_FAILURE,
error,
});
export const requestLikes = (
postId: number,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(likesRequestStart());
try {
const response = await fetch(`/posts/${postId}/likes`);
const json = await response.json();
dispatch(likesRequestSuccess(json));
} catch (e) {
dispatch(likesRequestFailure(e));
}
}

View File

@@ -2,20 +2,27 @@ import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import { State } from "../reducers/rootReducer";
import ILikeJSON from "../interfaces/json/ILike";
export const LIKE_SUBMIT_SUCCESS = 'LIKE_SUBMIT_SUCCESS';
interface LikeSubmitSuccessAction {
type: typeof LIKE_SUBMIT_SUCCESS,
postId: number;
isLike: boolean;
like: ILikeJSON;
}
export type LikeActionTypes = LikeSubmitSuccessAction;
const likeSubmitSuccess = (postId: number, isLike: boolean): LikeSubmitSuccessAction => ({
const likeSubmitSuccess = (
postId: number,
isLike: boolean,
like: ILikeJSON,
): LikeSubmitSuccessAction => ({
type: LIKE_SUBMIT_SUCCESS,
postId,
isLike,
like,
});
export const submitLike = (
@@ -32,9 +39,10 @@ export const submitLike = (
'X-CSRF-Token': authenticityToken,
},
});
const json = await res.json();
if (res.status === 201 || res.status === 202)
dispatch(likeSubmitSuccess(postId, isLike));
dispatch(likeSubmitSuccess(postId, isLike, json));
} catch (e) {
console.log('An error occurred while liking a post');
}

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
import ILike from '../../interfaces/ILike';
import Spinner from '../shared/Spinner';
import { DangerText } from '../shared/CustomTexts';
interface Props {
likes: Array<ILike>;
areLoading: boolean;
error: string;
}
const LikeList = ({ likes, areLoading, error}: Props) => (
<div className="likeList">
{ areLoading ? <Spinner /> : null }
{ error ? <DangerText>{error}</DangerText> : null }
{
likes.map((like, i) => (
<div className="like" key={i}>
{like.fullName}
</div>
))
}
</div>
);
export default LikeList;

View File

@@ -4,6 +4,8 @@ import IPost from '../../interfaces/IPost';
import IPostStatus from '../../interfaces/IPostStatus';
import IBoard from '../../interfaces/IBoard';
import LikeList from './LikeList';
import LikeButton from '../../containers/LikeButton';
import PostBoardSelect from './PostBoardSelect';
import PostStatusSelect from './PostStatusSelect';
import PostBoardLabel from '../shared/PostBoardLabel';
@@ -12,18 +14,21 @@ import Comments from '../../containers/Comments';
import { MutedText } from '../shared/CustomTexts';
import friendlyDate from '../../helpers/friendlyDate';
import { LikesState } from '../../reducers/likesReducer';
interface Props {
postId: number;
post: IPost;
likes: LikesState;
boards: Array<IBoard>;
postStatuses: Array<IPostStatus>;
isLoggedIn: boolean;
isPowerUser: boolean;
userEmail: string;
authenticityToken: string;
requestPost(postId: number): void;
requestLikes(postId: number): void;
changePostBoard(
postId: number,
newBoardId: number,
@@ -39,16 +44,19 @@ interface Props {
class PostP extends React.Component<Props> {
componentDidMount() {
this.props.requestPost(this.props.postId);
this.props.requestLikes(this.props.postId);
}
render() {
const {
post,
likes,
boards,
postStatuses,
isLoggedIn,
isPowerUser,
userEmail,
authenticityToken,
changePostBoard,
@@ -58,14 +66,23 @@ class PostP extends React.Component<Props> {
return (
<div className="pageContainer">
<div className="sidebar">
<div className="sidebarCard"></div>
<div className="sidebarCard"></div>
<div className="sidebarCard"></div>
<LikeList
likes={likes.items}
areLoading={likes.areLoading}
error={likes.error}
/>
</div>
<div className="postAndCommentsContainer">
<div className="postContainer">
<div className="postHeader">
<LikeButton
postId={post.id}
likesCount={likes.items.length}
liked={likes.items.find(like => like.email === userEmail) ? 1 : 0}
isLoggedIn={isLoggedIn}
authenticityToken={authenticityToken}
/>
<h2>{post.title}</h2>
{
isPowerUser && post ?

View File

@@ -17,6 +17,7 @@ interface Props {
postStatuses: Array<IPostStatus>;
isLoggedIn: boolean;
isPowerUser: boolean;
userEmail: string;
authenticityToken: string;
}
@@ -36,6 +37,7 @@ class PostRoot extends React.Component<Props> {
postStatuses,
isLoggedIn,
isPowerUser,
userEmail,
authenticityToken
} = this.props;
@@ -48,6 +50,7 @@ class PostRoot extends React.Component<Props> {
isLoggedIn={isLoggedIn}
isPowerUser={isPowerUser}
userEmail={userEmail}
authenticityToken={authenticityToken}
/>
</Provider>

View File

@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import { requestPost } from '../actions/requestPost';
import { requestLikes } from '../actions/requestLikes';
import { changePostBoard } from '../actions/changePostBoard';
import { changePostStatus } from '../actions/changePostStatus';
@@ -10,7 +11,7 @@ import PostP from '../components/Post/PostP';
const mapStateToProps = (state: State) => ({
post: state.currentPost.item,
comments: state.currentPost.comments,
likes: state.currentPost.likes,
});
const mapDispatchToProps = (dispatch) => ({
@@ -18,6 +19,10 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(requestPost(postId));
},
requestLikes(postId: number) {
dispatch(requestLikes(postId));
},
changePostBoard(postId: number, newBoardId: number, authenticityToken: string) {
dispatch(changePostBoard(postId, newBoardId, authenticityToken));
},

View File

@@ -0,0 +1,7 @@
interface ILike {
id: number;
fullName: string;
email: string;
}
export default ILike;

View File

@@ -0,0 +1,9 @@
interface ILikeJSON {
id: number;
user_id: number;
post_id: number;
full_name: string;
email: string;
}
export default ILikeJSON;

View File

@@ -15,6 +15,18 @@ import {
CHANGE_POST_STATUS_SUCCESS,
} from '../actions/changePostStatus';
import {
LikesRequestActionTypes,
LIKES_REQUEST_START,
LIKES_REQUEST_SUCCESS,
LIKES_REQUEST_FAILURE,
} from '../actions/requestLikes';
import {
LikeActionTypes,
LIKE_SUBMIT_SUCCESS,
} from '../actions/submitLike';
import {
CommentsRequestActionTypes,
COMMENTS_REQUEST_START,
@@ -36,8 +48,10 @@ import {
} from '../actions/submitComment';
import postReducer from './postReducer';
import likesReducer from './likesReducer';
import commentsReducer from './commentsReducer';
import { LikesState } from './likesReducer';
import { CommentsState } from './commentsReducer';
import IPost from '../interfaces/IPost';
@@ -46,6 +60,7 @@ interface CurrentPostState {
item: IPost;
isLoading: boolean;
error: string;
likes: LikesState;
comments: CommentsState;
}
@@ -53,6 +68,7 @@ const initialState: CurrentPostState = {
item: postReducer(undefined, {} as PostRequestActionTypes),
isLoading: false,
error: '',
likes: likesReducer(undefined, {} as LikesRequestActionTypes),
comments: commentsReducer(undefined, {} as CommentsRequestActionTypes),
};
@@ -62,6 +78,8 @@ const currentPostReducer = (
PostRequestActionTypes |
ChangePostBoardSuccessAction |
ChangePostStatusSuccessAction |
LikesRequestActionTypes |
LikeActionTypes |
CommentsRequestActionTypes |
HandleCommentRepliesType |
CommentSubmitActionTypes
@@ -95,6 +113,15 @@ const currentPostReducer = (
item: postReducer(state.item, action),
};
case LIKES_REQUEST_START:
case LIKES_REQUEST_SUCCESS:
case LIKES_REQUEST_FAILURE:
case LIKE_SUBMIT_SUCCESS:
return {
...state,
likes: likesReducer(state.likes, action),
};
case COMMENTS_REQUEST_START:
case COMMENTS_REQUEST_SUCCESS:
case COMMENTS_REQUEST_FAILURE:

View File

@@ -0,0 +1,82 @@
import {
LikesRequestActionTypes,
LIKES_REQUEST_START,
LIKES_REQUEST_SUCCESS,
LIKES_REQUEST_FAILURE,
} from '../actions/requestLikes';
import {
LikeActionTypes,
LIKE_SUBMIT_SUCCESS,
} from '../actions/submitLike';
import ILike from '../interfaces/ILike';
export interface LikesState {
items: Array<ILike>;
areLoading: boolean;
error: string;
}
const initialState: LikesState = {
items: [],
areLoading: false,
error: '',
};
const likesReducer = (
state = initialState,
action: LikesRequestActionTypes | LikeActionTypes,
) => {
switch (action.type) {
case LIKES_REQUEST_START:
return {
...state,
areLoading: true,
};
case LIKES_REQUEST_SUCCESS:
return {
...state,
items: action.likes.map(like => ({
id: like.id,
fullName: like.full_name,
email: like.email,
})),
areLoading: false,
error: '',
};
case LIKES_REQUEST_FAILURE:
return {
...state,
areLoading: false,
error: action.error,
};
case LIKE_SUBMIT_SUCCESS:
if (action.isLike) {
return {
...state,
items: [
{
id: action.like.id,
fullName: action.like.full_name,
email: action.like.email,
},
...state.items,
],
};
} else {
return {
...state,
items: state.items.filter(like => like.id !== action.like.id),
};
}
default:
return state;
}
}
export default likesReducer;

View File

@@ -12,6 +12,12 @@
.postAndCommentsContainer { width: 100%; }
}
.sidebar {
.likeList {
@extend .sidebarCard;
}
}
.postAndCommentsContainer {
@extend
.card,

View File

@@ -7,6 +7,7 @@
postStatuses: @post_statuses,
isLoggedIn: user_signed_in?,
isPowerUser: user_signed_in? ? current_user.power_user? : false,
userEmail: user_signed_in? ? current_user.email : nil,
authenticityToken: form_authenticity_token,
}
)

View File

@@ -16,6 +16,7 @@ Rails.application.routes.draw do
resources :posts, only: [:index, :create, :show, :update] do
resource :likes, only: [:create, :destroy]
resources :likes, only: [:index]
resources :comments, only: [:index, :create]
end
resources :boards, only: [:show]

View File

@@ -2,14 +2,16 @@ require 'rails_helper'
RSpec.describe 'likes routing', :aggregate_failures, type: :routing do
it 'routes likes' do
expect(get: '/posts/1/likes').to route_to(
controller: 'likes', action: 'index', post_id: '1'
)
expect(post: '/posts/1/likes').to route_to(
controller: 'likes', action: 'create', post_id: "1"
controller: 'likes', action: 'create', post_id: '1'
)
expect(delete: '/posts/1/likes').to route_to(
controller: 'likes', action: 'destroy', post_id: "1"
controller: 'likes', action: 'destroy', post_id: '1'
)
expect(get: '/posts/1/likes').not_to be_routable
expect(get: '/posts/1/likes/1').not_to be_routable
expect(get: '/posts/1/likes/new').not_to be_routable
expect(get: '/posts/1/likes/1/edit').not_to be_routable