mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 11:17:49 +01:00
Add basic version of post show page
This commit is contained in:
@@ -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
|
||||||
|
|||||||
42
app/javascript/actions/changePostStatus.ts
Normal file
42
app/javascript/actions/changePostStatus.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/javascript/actions/requestPost.ts
Normal file
57
app/javascript/actions/requestPost.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
62
app/javascript/components/Post/PostP.tsx
Normal file
62
app/javascript/components/Post/PostP.tsx
Normal 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;
|
||||||
37
app/javascript/components/Post/PostStatusSelect.tsx
Normal file
37
app/javascript/components/Post/PostStatusSelect.tsx
Normal 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;
|
||||||
23
app/javascript/components/Post/index.tsx
Normal file
23
app/javascript/components/Post/index.tsx
Normal 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;
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
27
app/javascript/containers/Post.tsx
Normal file
27
app/javascript/containers/Post.tsx
Normal 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);
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
0
app/javascript/stylesheets/components/Post.scss
Normal file
0
app/javascript/stylesheets/components/Post.scss
Normal file
12
app/views/posts/show.html.erb
Normal file
12
app/views/posts/show.html.erb
Normal 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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
%>
|
||||||
@@ -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}'
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user