mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add Likes in Post component
This commit is contained in:
@@ -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)
|
||||
|
||||
59
app/javascript/actions/requestLikes.ts
Normal file
59
app/javascript/actions/requestLikes.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
27
app/javascript/components/Post/LikeList.tsx
Normal file
27
app/javascript/components/Post/LikeList.tsx
Normal 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;
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
7
app/javascript/interfaces/ILike.ts
Normal file
7
app/javascript/interfaces/ILike.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
interface ILike {
|
||||
id: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default ILike;
|
||||
9
app/javascript/interfaces/json/ILike.ts
Normal file
9
app/javascript/interfaces/json/ILike.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
interface ILikeJSON {
|
||||
id: number;
|
||||
user_id: number;
|
||||
post_id: number;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default ILikeJSON;
|
||||
@@ -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:
|
||||
|
||||
82
app/javascript/reducers/likesReducer.ts
Normal file
82
app/javascript/reducers/likesReducer.ts
Normal 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;
|
||||
@@ -12,6 +12,12 @@
|
||||
.postAndCommentsContainer { width: 100%; }
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.likeList {
|
||||
@extend .sidebarCard;
|
||||
}
|
||||
}
|
||||
|
||||
.postAndCommentsContainer {
|
||||
@extend
|
||||
.card,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user