diff --git a/app/controllers/post_statuses_controller.rb b/app/controllers/post_statuses_controller.rb new file mode 100644 index 00000000..8de13bd9 --- /dev/null +++ b/app/controllers/post_statuses_controller.rb @@ -0,0 +1,7 @@ +class PostStatusesController < ApplicationController + def index + post_statuses = PostStatus.order(order: :asc) + + render json: post_statuses + end +end \ No newline at end of file diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index b533134e..3faf4e1b 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -3,11 +3,12 @@ class PostsController < ApplicationController def index_by_board_id board_id = params[:board_id] || 1 + posts = Post .left_outer_joins(:post_status) .select('posts.title, posts.description, post_statuses.name as post_status_name, post_statuses.color as post_status_color') - .where(board_id: board_id) + .where(filter_params) render json: posts end @@ -24,8 +25,13 @@ class PostsController < ApplicationController end private - + + def filter_params + params.permit(:board_id, :post_status_id) + end + def post_params params.require(:post).permit(:title, :description, :board_id) end + end diff --git a/app/javascript/components/Board/NewPost.tsx b/app/javascript/components/Board/NewPost.tsx index a69dff7c..6fe820f8 100644 --- a/app/javascript/components/Board/NewPost.tsx +++ b/app/javascript/components/Board/NewPost.tsx @@ -127,7 +127,7 @@ class NewPost extends React.Component { } = this.state; return ( -
+
{board.name} {board.description} {/* {this.props.authenticityToken} */} diff --git a/app/javascript/components/Board/PostList.tsx b/app/javascript/components/Board/PostList.tsx index 8c0ead74..06777f07 100644 --- a/app/javascript/components/Board/PostList.tsx +++ b/app/javascript/components/Board/PostList.tsx @@ -3,76 +3,30 @@ import * as React from 'react'; import PostListItem from './PostListItem'; import Spinner from '../shared/Spinner'; -import IBoard from '../../interfaces/IBoard'; import IPost from '../../interfaces/IPost'; interface Props { - board: IBoard; -} - -interface State { posts: Array; - isLoading: boolean; + areLoading: boolean; error: string; } -class PostList extends React.Component { - constructor(props) { - super(props); +const PostList = ({ posts, areLoading, error }: Props) => ( +
+ { areLoading ? : null } + { error ? {error} : null } + { + posts.map((post, i) => ( + ({ - title: post.title, - description: post.description, - postStatus: { - name: post.post_status_name, - color: post.post_status_color, - }, - })), - }); - } catch (e) { - this.setState({ - error: 'An unknown error occurred, try again.' - }); + key={i} + /> + )) } - } - - render() { - const { posts, isLoading, error } = this.state; - - return ( -
- { isLoading ? : null } - { error ? {error} : null } - { - posts.map((post, i) => ( - - )) - } -
- ); - } -} +
+); export default PostList; \ No newline at end of file diff --git a/app/javascript/components/Board/PostStatusFilter.tsx b/app/javascript/components/Board/PostStatusFilter.tsx new file mode 100644 index 00000000..3fa97369 --- /dev/null +++ b/app/javascript/components/Board/PostStatusFilter.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; + +import PostStatusListItem from './PostStatusListItem'; +import Spinner from '../shared/Spinner'; + +import IPostStatus from '../../interfaces/IPostStatus'; + +interface Props { + postStatuses: Array; + areLoading: boolean; + error: string; + + handleFilterClick(postStatusId: number): void; + currentFilter: number; +} + +const PostStatusFilter = ({ + postStatuses, + areLoading, + error, + + handleFilterClick, + currentFilter, +}: Props) => ( +
+ Filter by post status: + + { areLoading ? : null } + { error ? {error} : null } + { + postStatuses.map((postStatus, i) => ( + handleFilterClick(postStatus.id)} + isCurrentFilter={postStatus.id === currentFilter} + handleResetFilter={() => handleFilterClick(0)} + + key={i} + /> + )) + } +
+); + +export default PostStatusFilter; \ No newline at end of file diff --git a/app/javascript/components/Board/PostStatusListItem.tsx b/app/javascript/components/Board/PostStatusListItem.tsx new file mode 100644 index 00000000..64964f40 --- /dev/null +++ b/app/javascript/components/Board/PostStatusListItem.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +interface Props { + name: string; + color: string; + + handleClick(): void; + isCurrentFilter: boolean; + handleResetFilter(): void; +} + +const PostStatusListItem = ({ + name, + color, + handleClick, + isCurrentFilter, + handleResetFilter, +}: Props) => ( +
+ +
+
+ {name} +
+
+ { + isCurrentFilter ? + : null + } +
+); + +export default PostStatusListItem; \ No newline at end of file diff --git a/app/javascript/components/Board/index.tsx b/app/javascript/components/Board/index.tsx index b9bc207a..ab36ce09 100644 --- a/app/javascript/components/Board/index.tsx +++ b/app/javascript/components/Board/index.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import NewPost from './NewPost'; +import PostStatusFilter from './PostStatusFilter'; import PostList from './PostList'; import IBoard from '../../interfaces/IBoard'; +import IPost from '../../interfaces/IPost'; +import IPostStatus from '../../interfaces/IPostStatus'; import '../../stylesheets/components/Board.scss'; @@ -13,16 +16,147 @@ interface Props { authenticityToken: string; } -class Board extends React.Component { +interface State { + filters: { + byPostStatus: number; + }; + posts: { + items: Array; + areLoading: boolean; + error: string; + }; + postStatuses: { + items: Array; + areLoading: boolean; + error: string; + }; +} + +class Board extends React.Component { + constructor(props) { + super(props); + + this.state = { + filters: { + byPostStatus: 0, + }, + posts: { + items: [], + areLoading: true, + error: '', + }, + postStatuses: { + items: [], + areLoading: true, + error: '', + } + }; + + this.requestPosts = this.requestPosts.bind(this); + this.requestPostStatuses = this.requestPostStatuses.bind(this); + this.setPostStatusFilter = this.setPostStatusFilter.bind(this); + } + + componentDidMount() { + this.requestPosts(); + this.requestPostStatuses(); + } + + async requestPosts() { + this.setState({ + posts: { ...this.state.posts, areLoading: true }, + }); + + const boardId = this.props.board.id; + const { byPostStatus } = this.state.filters; + + let params = ''; + if (byPostStatus) params += `&post_status_id=${byPostStatus}`; + + try { + let res = await fetch(`http://localhost:3000/posts?board_id=${boardId}${params}`); + let data = await res.json(); + + this.setState({ + posts: { + items: data.map(post => ({ + title: post.title, + description: post.description, + postStatus: { + name: post.post_status_name, + color: post.post_status_color, + }, + })), + areLoading: false, + error: '', + } + }); + } catch (e) { + this.setState({ + posts: { ...this.state.posts, error: 'An unknown error occurred, try again.' }, + }); + } + } + + setPostStatusFilter(postStatusId: number) { + this.setState({ + filters: { byPostStatus: postStatusId }, + }, this.requestPosts); + } + + async requestPostStatuses() { + this.setState({ + postStatuses: { ...this.state.postStatuses, areLoading: true }, + }); + + try { + let res = await fetch('http://localhost:3000/post_statuses'); + let data = await res.json(); + + this.setState({ + postStatuses: { + items: data.map(postStatus => ({ + id: postStatus.id, + name: postStatus.name, + color: postStatus.color, + })), + areLoading: false, + error: '', + }, + }); + } catch (e) { + this.setState({ + postStatuses: { ...this.state.postStatuses, error: 'An unknown error occurred, try again.' }, + }); + } + } + render() { const { board, isLoggedIn, authenticityToken } = this.props; + const { posts, postStatuses } = this.state; return (
- + +
- +
); } diff --git a/app/javascript/stylesheets/components/Board.scss b/app/javascript/stylesheets/components/Board.scss index c1f4a92d..0eeb7921 100644 --- a/app/javascript/stylesheets/components/Board.scss +++ b/app/javascript/stylesheets/components/Board.scss @@ -5,27 +5,36 @@ align-items: flex-start; flex-wrap: nowrap; + .smallTitle { + font-size: 18px; + font-weight: 500; + } + .sidebar { top: 20px; position: sticky; + + .sidebar-box { + flex: 0 0 auto; + + width: 250px; + + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } } - .newBoardContainer { - flex: 0 0 auto; - - width: 250px; - - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - + .box { border: 1px solid black; border-radius: 4px; padding: 8px; margin: 8px; + } + .newBoardContainer { .boardName { font-size: 24px; font-weight: 600; @@ -57,18 +66,53 @@ text-align: center; } } + + .postStatusFilterContainer { + .postStatusListItemContainer { + display: flex; + + flex: 1 1 auto; + align-self: stretch; + } + + .postStatusListItemLink { + flex: 1 1 auto; + } + + .postStatusListItem { + height: 40px; + + display: flex; + align-items: center; + + text-transform: uppercase; + + padding: 4px; + } + + .postStatusListItem:hover { + cursor: pointer; + + background-color: #f5f5f5; + border-radius: 4px; + } + + .resetFilter { + flex: 0 0 auto; + width: 30px; + height: 30px; + + padding: 0; + + align-self: center; + } + } .postList { display: flex; flex-direction: column; flex: 1 1 auto; - - border: 1px solid black; - border-radius: 4px; - - padding: 8px; - margin: 8px; .postLink { text-decoration: none; @@ -117,16 +161,17 @@ display: flex; } - .dot { - width: 16px; - height: 16px; - border-radius: 100%; - - margin-top: auto; - margin-bottom: auto; - margin-right: 4px; - } } } } +} + +.dot { + width: 16px; + height: 16px; + border-radius: 100%; + + margin-top: auto; + margin-bottom: auto; + margin-right: 4px; } \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index b5feb9ce..c6b9b54a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,4 +16,5 @@ Rails.application.routes.draw do post '/posts', to: 'posts#create' get '/posts', to: 'posts#index_by_board_id' + get '/post_statuses', to: 'post_statuses#index' end