Add basic version of post show page

This commit is contained in:
riggraz
2019-09-12 15:51:45 +02:00
parent 5ca113b545
commit f599471af1
18 changed files with 330 additions and 11 deletions

View File

@@ -1,9 +1,9 @@
class PostsController < ApplicationController class PostsController < ApplicationController
before_action :authenticate_user!, only: [:create] before_action :authenticate_user!, only: [:create, :update]
def index def index
posts = Post posts = Post
.select(:title, :description, :post_status_id) .select(:id, :title, :description, :post_status_id)
.where(filter_params) .where(filter_params)
.search_by_name_or_description(params[:search]) .search_by_name_or_description(params[:search])
.page(params[:page]) .page(params[:page])
@@ -23,6 +23,37 @@ class PostsController < ApplicationController
end end
end end
def show
@post = Post.find(params[:id])
@post_statuses = PostStatus
.find_roadmap
.select(:id, :name, :color)
respond_to do |format|
format.html
format.json { render json: @post }
end
end
def update
post = Post.find(params[:id])
if current_user.role == :user && current_user.id != post.user_id
render json: I18n.t('errors.unauthorized'), status: :unauthorized
end
post.post_status_id = params[:post][:post_status_id]
if post.save
render json: post, status: :no_content
else
render json: {
error: I18n.t('errors.post.update', message: post.errors.full_messages)
}, status: :unprocessable_entity
end
end
private private
def filter_params def filter_params

View File

@@ -0,0 +1,42 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { State } from '../reducers/rootReducer';
export const CHANGE_POST_STATUS_SUCCESS = 'CHANGE_POST_STATUS_SUCCESS';
interface ChangePostStatusSuccessAction {
type: typeof CHANGE_POST_STATUS_SUCCESS;
newPostStatusId;
}
const changePostStatusSuccess = (newPostStatusId: number): ChangePostStatusSuccessAction => ({
type: CHANGE_POST_STATUS_SUCCESS,
newPostStatusId,
});
export const changePostStatus = (
postId: number,
newPostStatusId: number,
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
try {
const response = await fetch(`/posts/${postId}`, {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': authenticityToken,
},
body: JSON.stringify({
post: {
post_status_id: newPostStatusId,
},
})
});
if (response.status === 204) {
dispatch(changePostStatusSuccess(newPostStatusId));
}
} catch (e) {
console.log(e);
}
}

View File

@@ -0,0 +1,57 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import IPostJSON from '../interfaces/json/IPost';
import { State } from '../reducers/rootReducer';
export const POST_REQUEST_START = 'POST_REQUEST_START';
interface PostRequestStartAction {
type: typeof POST_REQUEST_START;
}
export const POST_REQUEST_SUCCESS = 'POST_REQUEST_SUCCESS';
interface PostRequestSuccessAction {
type: typeof POST_REQUEST_SUCCESS;
post: IPostJSON;
}
export const POST_REQUEST_FAILURE = 'POST_REQUEST_FAILURE';
interface PostRequestFailureAction {
type: typeof POST_REQUEST_FAILURE;
error: string;
}
export type PostRequestActionTypes =
PostRequestStartAction |
PostRequestSuccessAction |
PostRequestFailureAction;
const postRequestStart = (): PostRequestActionTypes => ({
type: POST_REQUEST_START,
});
export const postRequestSuccess = (post: IPostJSON): PostRequestActionTypes => ({
type: POST_REQUEST_SUCCESS,
post,
});
const postRequestFailure = (error: string): PostRequestActionTypes => ({
type: POST_REQUEST_FAILURE,
error,
});
export const requestPost = (
postId: number,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(postRequestStart());
try {
const response = await fetch(`/posts/${postId}.json`);
const json = await response.json();
dispatch(postRequestSuccess(json));
} catch (e) {
dispatch(postRequestFailure(e));
}
}

View File

@@ -42,6 +42,7 @@ const PostList = ({
posts.length > 0 ? posts.length > 0 ?
posts.map((post, i) => ( posts.map((post, i) => (
<PostListItem <PostListItem
id={post.id}
title={post.title} title={post.title}
description={post.description} description={post.description}
postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)} postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)}

View File

@@ -3,13 +3,14 @@ import * as React from 'react';
import IPostStatus from '../../interfaces/IPostStatus'; import IPostStatus from '../../interfaces/IPostStatus';
interface Props { interface Props {
id: number;
title: string; title: string;
description?: string; description?: string;
postStatus: IPostStatus; postStatus: IPostStatus;
} }
const PostListItem = ({ title, description, postStatus}: Props) => ( const PostListItem = ({ id, title, description, postStatus}: Props) => (
<a href="#" className="postLink"> <a href={`/posts/${id}`} className="postLink">
<div className="postListItem"> <div className="postListItem">
<div className="postTitle">{title}</div> <div className="postTitle">{title}</div>
<div className="postDescription"> <div className="postDescription">

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import IPost from '../../interfaces/IPost';
import IPostStatus from '../../interfaces/IPostStatus';
import PostStatusSelect from './PostStatusSelect';
interface Props {
postId: number;
post: IPost;
postStatuses: Array<IPostStatus>;
isLoggedIn: boolean;
isPowerUser: boolean;
authenticityToken: string;
requestPost(postId: number): void;
changePostStatus(
postId: number,
newPostStatusId: number,
authenticityToken: string,
): void;
}
class PostP extends React.Component<Props> {
componentDidMount() {
this.props.requestPost(this.props.postId);
}
render() {
const {
post,
postStatuses,
isPowerUser,
authenticityToken,
changePostStatus,
} = this.props;
return (
<div>
<h1>{post.title}</h1>
{
isPowerUser ?
<PostStatusSelect
postStatuses={postStatuses}
selectedPostStatusId={post.postStatusId}
handleChange={
newPostStatusId => changePostStatus(post.id, newPostStatusId, authenticityToken)
}
/>
:
<span>LLL</span>
}
<p>{post.description}</p>
</div>
);
}
}
export default PostP;

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { FormEvent } from 'react';
import IPostStatus from '../../interfaces/IPostStatus';
interface Props {
postStatuses: Array<IPostStatus>;
selectedPostStatusId: number;
handleChange(
newPostStatusId: number,
): void;
}
const PostStatusSelect = ({
postStatuses,
selectedPostStatusId,
handleChange,
}: Props) => (
<select
value={selectedPostStatusId || ''}
onChange={
(e: FormEvent) => (
handleChange(parseInt((e.target as HTMLSelectElement).value))
)}
className="selectPicker"
>
{postStatuses.map((postStatus, i) => (
<option value={postStatus.id} key={i}>
{postStatus.name}
</option>
))}
<option>None</option>
</select>
);
export default PostStatusSelect;

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import store from '../../stores';
import Post from '../../containers/Post';
import '../../stylesheets/components/Post.scss';
const PostRoot = ({ postId, postStatuses, isLoggedIn, isPowerUser, authenticityToken }) => (
<Provider store={store}>
<Post
postId={postId}
postStatuses={postStatuses}
isLoggedIn={isLoggedIn}
isPowerUser={isPowerUser}
authenticityToken={authenticityToken}
/>
</Provider>
);
export default PostRoot;

View File

@@ -16,6 +16,7 @@ const PostList = ({ posts, boards }: Props) => (
posts.length > 0 ? posts.length > 0 ?
posts.map((post, i) => ( posts.map((post, i) => (
<PostListItem <PostListItem
id={post.id}
title={post.title} title={post.title}
boardName={boards.find(board => board.id === post.board_id).name} boardName={boards.find(board => board.id === post.board_id).name}

View File

@@ -1,12 +1,13 @@
import * as React from 'react'; import * as React from 'react';
interface Props { interface Props {
id: number;
title: string; title: string;
boardName: string; boardName: string;
} }
const PostListItem = ({title, boardName}: Props) => ( const PostListItem = ({id, title, boardName}: Props) => (
<a href="#" className="postLink"> <a href={`/posts/${id}`} className="postLink">
<div className="postListItem"> <div className="postListItem">
<div className="postTitle">{title}</div> <div className="postTitle">{title}</div>
<div className="postBoard">{boardName}</div> <div className="postBoard">{boardName}</div>

View File

@@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { requestPost } from '../actions/requestPost';
import { changePostStatus } from '../actions/changePostStatus';
import { State } from '../reducers/rootReducer';
import PostP from '../components/Post/PostP';
const mapStateToProps = (state: State) => ({
post: state.currentPost,
});
const mapDispatchToProps = (dispatch) => ({
requestPost(postId: number) {
dispatch(requestPost(postId));
},
changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) {
dispatch(changePostStatus(postId, newPostStatusId, authenticityToken));
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PostP);

View File

@@ -1,3 +1,13 @@
import {
POST_REQUEST_START,
POST_REQUEST_SUCCESS,
POST_REQUEST_FAILURE,
} from '../actions/requestPost';
import {
CHANGE_POST_STATUS_SUCCESS,
} from '../actions/changePostStatus';
import IPost from '../interfaces/IPost'; import IPost from '../interfaces/IPost';
const initialState: IPost = { const initialState: IPost = {
@@ -15,7 +25,7 @@ const postReducer = (
action, action,
): IPost => { ): IPost => {
switch (action.type) { switch (action.type) {
case 'CONVERT': case POST_REQUEST_SUCCESS:
return { return {
id: action.post.id, id: action.post.id,
title: action.post.title, title: action.post.title,
@@ -26,6 +36,14 @@ const postReducer = (
createdAt: action.post.created_at, createdAt: action.post.created_at,
}; };
case CHANGE_POST_STATUS_SUCCESS:
return {
...state,
postStatusId: action.newPostStatusId,
};
case POST_REQUEST_START:
case POST_REQUEST_FAILURE:
default: default:
return state; return state;
} }

View File

@@ -12,6 +12,8 @@ import {
POSTS_REQUEST_FAILURE, POSTS_REQUEST_FAILURE,
} from '../actions/requestPosts'; } from '../actions/requestPosts';
import { postRequestSuccess } from '../actions/requestPost';
import { import {
ChangeFiltersActionTypes, ChangeFiltersActionTypes,
SET_SEARCH_FILTER, SET_SEARCH_FILTER,
@@ -54,9 +56,9 @@ const postsReducer = (
return { return {
...state, ...state,
items: action.page === 1 ? items: action.page === 1 ?
action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post})) //improve action.posts.map(post => postReducer(undefined, postRequestSuccess(post)))
: :
[...state.items, ...action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post}))], [...state.items, ...action.posts.map(post => postReducer(undefined, postRequestSuccess(post)))],
page: action.page, page: action.page,
haveMore: action.posts.length === 15, haveMore: action.posts.length === 15,
areLoading: false, areLoading: false,

View File

@@ -2,10 +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';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
posts: postsReducer, posts: postsReducer,
postStatuses: postStatusesReducer, postStatuses: postStatusesReducer,
currentPost: postReducer,
}); });
export type State = ReturnType<typeof rootReducer> export type State = ReturnType<typeof rootReducer>

View File

@@ -0,0 +1,12 @@
<%=
react_component(
'Post',
{
postId: @post.id,
postStatuses: @post_statuses,
isLoggedIn: user_signed_in?,
isPowerUser: user_signed_in? ? (current_user.role == 'admin' || current_user.role == 'moderator') : false,
authenticityToken: form_authenticity_token,
}
)
%>

View File

@@ -1,4 +1,6 @@
en: en:
errors: errors:
unauthorized: 'You are not authorized'
post: post:
create: 'Post create error: %{message}' create: 'Post create error: %{message}'
update: 'Post update error: %{message}'

View File

@@ -13,6 +13,6 @@ Rails.application.routes.draw do
devise_for :users devise_for :users
resources :boards, only: [:show] resources :boards, only: [:show]
resources :posts, only: [:index, :create] resources :posts, only: [:index, :create, :show, :update]
resources :post_statuses, only: [:index] resources :post_statuses, only: [:index]
end end