diff --git a/app/controllers/likes_controller.rb b/app/controllers/likes_controller.rb index 8b85d338..261cdcfb 100644 --- a/app/controllers/likes_controller.rb +++ b/app/controllers/likes_controller.rb @@ -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) diff --git a/app/javascript/actions/requestLikes.ts b/app/javascript/actions/requestLikes.ts new file mode 100644 index 00000000..914292ed --- /dev/null +++ b/app/javascript/actions/requestLikes.ts @@ -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; +} + +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, +): LikesRequestActionTypes => ({ + type: LIKES_REQUEST_SUCCESS, + likes, +}); + +const likesRequestFailure = (error: string): LikesRequestActionTypes => ({ + type: LIKES_REQUEST_FAILURE, + error, +}); + +export const requestLikes = ( + postId: number, +): ThunkAction> => 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)); + } +} \ No newline at end of file diff --git a/app/javascript/actions/submitLike.ts b/app/javascript/actions/submitLike.ts index 26c64ca8..426a6727 100644 --- a/app/javascript/actions/submitLike.ts +++ b/app/javascript/actions/submitLike.ts @@ -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'); } diff --git a/app/javascript/components/Post/LikeList.tsx b/app/javascript/components/Post/LikeList.tsx new file mode 100644 index 00000000..d651905a --- /dev/null +++ b/app/javascript/components/Post/LikeList.tsx @@ -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; + areLoading: boolean; + error: string; +} + +const LikeList = ({ likes, areLoading, error}: Props) => ( +
+ { areLoading ? : null } + { error ? {error} : null } + { + likes.map((like, i) => ( +
+ {like.fullName} +
+ )) + } +
+); + +export default LikeList; \ No newline at end of file diff --git a/app/javascript/components/Post/PostP.tsx b/app/javascript/components/Post/PostP.tsx index 315abcb1..7dbf4b19 100644 --- a/app/javascript/components/Post/PostP.tsx +++ b/app/javascript/components/Post/PostP.tsx @@ -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; postStatuses: Array; 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 { 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 { return (
-
-
-
+
+ like.email === userEmail) ? 1 : 0} + isLoggedIn={isLoggedIn} + authenticityToken={authenticityToken} + />

{post.title}

{ isPowerUser && post ? diff --git a/app/javascript/components/Post/index.tsx b/app/javascript/components/Post/index.tsx index 397d6b73..f7f95a78 100644 --- a/app/javascript/components/Post/index.tsx +++ b/app/javascript/components/Post/index.tsx @@ -17,6 +17,7 @@ interface Props { postStatuses: Array; isLoggedIn: boolean; isPowerUser: boolean; + userEmail: string; authenticityToken: string; } @@ -36,6 +37,7 @@ class PostRoot extends React.Component { postStatuses, isLoggedIn, isPowerUser, + userEmail, authenticityToken } = this.props; @@ -48,6 +50,7 @@ class PostRoot extends React.Component { isLoggedIn={isLoggedIn} isPowerUser={isPowerUser} + userEmail={userEmail} authenticityToken={authenticityToken} /> diff --git a/app/javascript/containers/Post.tsx b/app/javascript/containers/Post.tsx index aefdf247..754d1672 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 { 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)); }, diff --git a/app/javascript/interfaces/ILike.ts b/app/javascript/interfaces/ILike.ts new file mode 100644 index 00000000..9aedc5a7 --- /dev/null +++ b/app/javascript/interfaces/ILike.ts @@ -0,0 +1,7 @@ +interface ILike { + id: number; + fullName: string; + email: string; +} + +export default ILike; \ No newline at end of file diff --git a/app/javascript/interfaces/json/ILike.ts b/app/javascript/interfaces/json/ILike.ts new file mode 100644 index 00000000..177b4885 --- /dev/null +++ b/app/javascript/interfaces/json/ILike.ts @@ -0,0 +1,9 @@ +interface ILikeJSON { + id: number; + user_id: number; + post_id: number; + full_name: string; + email: string; +} + +export default ILikeJSON; \ No newline at end of file diff --git a/app/javascript/reducers/currentPostReducer.ts b/app/javascript/reducers/currentPostReducer.ts index ea0a42c6..b1d48547 100644 --- a/app/javascript/reducers/currentPostReducer.ts +++ b/app/javascript/reducers/currentPostReducer.ts @@ -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: diff --git a/app/javascript/reducers/likesReducer.ts b/app/javascript/reducers/likesReducer.ts new file mode 100644 index 00000000..39b7d957 --- /dev/null +++ b/app/javascript/reducers/likesReducer.ts @@ -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; + 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; \ No newline at end of file diff --git a/app/javascript/stylesheets/components/Post.scss b/app/javascript/stylesheets/components/Post.scss index aa8f3c2c..c74451d4 100644 --- a/app/javascript/stylesheets/components/Post.scss +++ b/app/javascript/stylesheets/components/Post.scss @@ -4,13 +4,19 @@ .justify-content-between, .align-items-start; - flex-direction: row; + flex-direction: row; - @include media-breakpoint-down(sm) { - flex-direction: column; - - .postAndCommentsContainer { width: 100%; } - } + @include media-breakpoint-down(sm) { + flex-direction: column; + + .postAndCommentsContainer { width: 100%; } + } + + .sidebar { + .likeList { + @extend .sidebarCard; + } + } .postAndCommentsContainer { @extend diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index 0a1ebf28..c4f7eb35 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -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, } ) diff --git a/config/routes.rb b/config/routes.rb index bed5ae49..b5d991bd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/spec/routing/likes_routing_spec.rb b/spec/routing/likes_routing_spec.rb index eeaab3b7..5bbd5e20 100644 --- a/spec/routing/likes_routing_spec.rb +++ b/spec/routing/likes_routing_spec.rb @@ -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