From 8d9ab0f717a7e8ed628c26eb36c6fae21d742aeb Mon Sep 17 00:00:00 2001 From: riggraz Date: Wed, 11 Sep 2019 18:30:59 +0200 Subject: [PATCH] Add Redux and use it for state management --- app/controllers/posts_controller.rb | 3 +- app/javascript/actions/changeFilters.ts | 27 ++ app/javascript/actions/requestPostStatuses.ts | 59 +++++ app/javascript/actions/requestPosts.ts | 68 +++++ app/javascript/components/Board/BoardP.tsx | 112 +++++++++ app/javascript/components/Board/PostList.tsx | 14 +- .../components/Board/PostListItem.tsx | 17 +- app/javascript/components/Board/index.tsx | 235 +----------------- .../components/Roadmap/PostList.tsx | 4 +- .../Roadmap/PostListByPostStatus.tsx | 4 +- app/javascript/components/Roadmap/index.tsx | 4 +- app/javascript/containers/Board.tsx | 40 +++ app/javascript/interfaces/IPost.ts | 11 +- app/javascript/interfaces/json/IPost.ts | 11 + app/javascript/reducers/filtersReducer.ts | 39 +++ app/javascript/reducers/postReducer.ts | 34 +++ .../reducers/postStatusesReducer.ts | 57 +++++ app/javascript/reducers/postsReducer.ts | 85 +++++++ app/javascript/reducers/rootReducer.ts | 12 + app/javascript/stores/index.ts | 15 ++ package.json | 3 + yarn.lock | 48 +++- 22 files changed, 656 insertions(+), 246 deletions(-) create mode 100644 app/javascript/actions/changeFilters.ts create mode 100644 app/javascript/actions/requestPostStatuses.ts create mode 100644 app/javascript/actions/requestPosts.ts create mode 100644 app/javascript/components/Board/BoardP.tsx create mode 100644 app/javascript/containers/Board.tsx create mode 100644 app/javascript/interfaces/json/IPost.ts create mode 100644 app/javascript/reducers/filtersReducer.ts create mode 100644 app/javascript/reducers/postReducer.ts create mode 100644 app/javascript/reducers/postStatusesReducer.ts create mode 100644 app/javascript/reducers/postsReducer.ts create mode 100644 app/javascript/reducers/rootReducer.ts create mode 100644 app/javascript/stores/index.ts diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index e09a8184..16258aa7 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -3,8 +3,7 @@ class PostsController < ApplicationController def index 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') + .select(:title, :description, :post_status_id) .where(filter_params) .search_by_name_or_description(params[:search]) .page(params[:page]) diff --git a/app/javascript/actions/changeFilters.ts b/app/javascript/actions/changeFilters.ts new file mode 100644 index 00000000..5a26917e --- /dev/null +++ b/app/javascript/actions/changeFilters.ts @@ -0,0 +1,27 @@ +export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'; +interface SetSearchFilterAction { + type: typeof SET_SEARCH_FILTER; + searchQuery: string; +} + +export const SET_POST_STATUS_FILTER = 'SET_POST_STATUS_FILTER'; +interface SetPostStatusFilterAction { + type: typeof SET_POST_STATUS_FILTER; + postStatusId: number; +} + + +export const setSearchFilter = (searchQuery: string): SetSearchFilterAction => ({ + type: SET_SEARCH_FILTER, + searchQuery, +}); + +export const setPostStatusFilter = (postStatusId: number): SetPostStatusFilterAction => ({ + type: SET_POST_STATUS_FILTER, + postStatusId, +}); + + +export type ChangeFiltersActionTypes = + SetSearchFilterAction | + SetPostStatusFilterAction; \ No newline at end of file diff --git a/app/javascript/actions/requestPostStatuses.ts b/app/javascript/actions/requestPostStatuses.ts new file mode 100644 index 00000000..d4f2315d --- /dev/null +++ b/app/javascript/actions/requestPostStatuses.ts @@ -0,0 +1,59 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import IPostStatus from '../interfaces/IPostStatus'; + +import { State } from '../reducers/rootReducer'; + +export const POST_STATUSES_REQUEST_START = 'POST_STATUSES_REQUEST_START'; +interface PostStatusesRequestStartAction { + type: typeof POST_STATUSES_REQUEST_START; +} + +export const POST_STATUSES_REQUEST_SUCCESS = 'POST_STATUSES_REQUEST_SUCCESS'; +interface PostStatusesRequestSuccessAction { + type: typeof POST_STATUSES_REQUEST_SUCCESS; + postStatuses: Array; +} + +export const POST_STATUSES_REQUEST_FAILURE = 'POST_STATUSES_REQUEST_FAILURE'; +interface PostStatusesRequestFailureAction { + type: typeof POST_STATUSES_REQUEST_FAILURE; + error: string; +} + +export type PostStatusesRequestActionTypes = + PostStatusesRequestStartAction | + PostStatusesRequestSuccessAction | + PostStatusesRequestFailureAction + + +const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({ + type: POST_STATUSES_REQUEST_START, +}); + +const postStatusesRequestSuccess = ( + postStatuses: Array +): PostStatusesRequestActionTypes => ({ + type: POST_STATUSES_REQUEST_SUCCESS, + postStatuses, +}); + +const postStatusesRequestFailure = (error: string): PostStatusesRequestActionTypes => ({ + type: POST_STATUSES_REQUEST_FAILURE, + error, +}); + +export const requestPostStatuses = (): ThunkAction> => ( + async (dispatch) => { + dispatch(postStatusesRequestStart()); + + try { + const response = await fetch('/post_statuses'); + const json = await response.json(); + dispatch(postStatusesRequestSuccess(json)); + } catch (e) { + dispatch(postStatusesRequestFailure(e)); + } + } +) \ No newline at end of file diff --git a/app/javascript/actions/requestPosts.ts b/app/javascript/actions/requestPosts.ts new file mode 100644 index 00000000..1030cf35 --- /dev/null +++ b/app/javascript/actions/requestPosts.ts @@ -0,0 +1,68 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import IPostJSON from '../interfaces/json/IPost'; + +import { State } from '../reducers/rootReducer'; + +export const POSTS_REQUEST_START = 'POSTS_REQUEST_START'; +interface PostsRequestStartAction { + type: typeof POSTS_REQUEST_START; +} + +export const POSTS_REQUEST_SUCCESS = 'POSTS_REQUEST_SUCCESS'; +interface PostsRequestSuccessAction { + type: typeof POSTS_REQUEST_SUCCESS; + posts: Array; + page: number; +} + +export const POSTS_REQUEST_FAILURE = 'POSTS_REQUEST_FAILURE'; +interface PostsRequestFailureAction { + type: typeof POSTS_REQUEST_FAILURE; + error: string; +} + +export type PostsRequestActionTypes = + PostsRequestStartAction | + PostsRequestSuccessAction | + PostsRequestFailureAction; + + +const postsRequestStart = (): PostsRequestActionTypes => ({ + type: POSTS_REQUEST_START, +}); + +const postsRequestSuccess = (posts: Array, page: number): PostsRequestActionTypes => ({ + type: POSTS_REQUEST_SUCCESS, + posts, + page, +}); + +const postsRequestFailure = (error: string): PostsRequestActionTypes => ({ + type: POSTS_REQUEST_FAILURE, + error, +}); + +export const requestPosts = ( + boardId: number, + page: number, + searchQuery: string, + postStatusId: number, +): ThunkAction> => async (dispatch) => { + dispatch(postsRequestStart()); + + try { + let params = ''; + params += `page=${page}`; + params += `&board_id=${boardId}`; + if (searchQuery) params += `&search=${searchQuery}`; + if (postStatusId) params += `&post_status_id=${postStatusId}`; + + const response = await fetch(`/posts?${params}`); + const json = await response.json(); + dispatch(postsRequestSuccess(json, page)); + } catch (e) { + dispatch(postsRequestFailure(e)); + } +} \ No newline at end of file diff --git a/app/javascript/components/Board/BoardP.tsx b/app/javascript/components/Board/BoardP.tsx new file mode 100644 index 00000000..ed1537d8 --- /dev/null +++ b/app/javascript/components/Board/BoardP.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; + +import NewPost from './NewPost'; +import SearchFilter from './SearchFilter'; +import PostStatusFilter from './PostStatusFilter'; +import PostList from './PostList'; + +import IBoard from '../../interfaces/IBoard'; +import IPost from '../../interfaces/IPost'; + +import { PostsState } from '../../reducers/postsReducer'; +import { PostStatusesState } from '../../reducers/postStatusesReducer'; + +interface Props { + board: IBoard; + isLoggedIn: boolean; + authenticityToken: string; + posts: PostsState; + postStatuses: PostStatusesState; + + requestPosts( + boardId: number, + page?: number, + searchQuery?: string, + postStatusId?: number, + ): void; + requestPostStatuses(): void; + handleSearchFilterChange(searchQuery: string): void; + handlePostStatusFilterChange(postStatusId: number): void; +} + +class BoardP extends React.Component { + searchFilterTimeoutId: ReturnType; + + componentDidMount() { + this.props.requestPosts(this.props.board.id); + this.props.requestPostStatuses(); + } + + componentDidUpdate(prevProps) { + const { searchQuery } = this.props.posts.filters; + const prevSearchQuery = prevProps.posts.filters.searchQuery; + + const { postStatusId } = this.props.posts.filters; + const prevPostStatusId = prevProps.posts.filters.postStatusId; + + // search filter changed + if (searchQuery !== prevSearchQuery) { + if (this.searchFilterTimeoutId) clearInterval(this.searchFilterTimeoutId); + + this.searchFilterTimeoutId = setTimeout(() => ( + this.props.requestPosts(this.props.board.id, 1, searchQuery, postStatusId) + ), 500); + } + + // post status filter changed + if (postStatusId !== prevPostStatusId) { + this.props.requestPosts(this.props.board.id, 1, searchQuery, postStatusId); + } + } + + render() { + const { + board, + isLoggedIn, + authenticityToken, + posts, + postStatuses, + + requestPosts, + handleSearchFilterChange, + handlePostStatusFilterChange, + } = this.props; + + return ( +
+
+ + + +
+ + requestPosts(board.id, posts.page + 1)} + /> +
+ ); + } +} + +export default BoardP; \ No newline at end of file diff --git a/app/javascript/components/Board/PostList.tsx b/app/javascript/components/Board/PostList.tsx index dd9c9e57..2e298ec6 100644 --- a/app/javascript/components/Board/PostList.tsx +++ b/app/javascript/components/Board/PostList.tsx @@ -6,9 +6,11 @@ import PostListItem from './PostListItem'; import Spinner from '../shared/Spinner'; import IPost from '../../interfaces/IPost'; +import IPostStatus from '../../interfaces/IPostStatus'; interface Props { posts: Array; + postStatuses: Array; areLoading: boolean; error: string; @@ -17,7 +19,15 @@ interface Props { hasMore: boolean; } -const PostList = ({ posts, areLoading, error, handleLoadMore, page, hasMore }: Props) => ( +const PostList = ({ + posts, + postStatuses, + areLoading, + error, + handleLoadMore, + page, + hasMore +}: Props) => (
{ error ? {error} : null } postStatus.id === post.postStatusId)} key={i} /> diff --git a/app/javascript/components/Board/PostListItem.tsx b/app/javascript/components/Board/PostListItem.tsx index b3a2ee56..282b5c82 100644 --- a/app/javascript/components/Board/PostListItem.tsx +++ b/app/javascript/components/Board/PostListItem.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; +import IPostStatus from '../../interfaces/IPostStatus'; + interface Props { title: string; description?: string; - postStatus: {name: string, color: string}; + postStatus: IPostStatus; } const PostListItem = ({ title, description, postStatus}: Props) => ( @@ -23,10 +25,15 @@ const PostListItem = ({ title, description, postStatus}: Props) => ( 0 comments
-
-
- {postStatus.name} -
+ { + postStatus ? +
+
+ {postStatus.name} +
+ : + null + } diff --git a/app/javascript/components/Board/index.tsx b/app/javascript/components/Board/index.tsx index 4dc6a439..b060221b 100644 --- a/app/javascript/components/Board/index.tsx +++ b/app/javascript/components/Board/index.tsx @@ -1,229 +1,20 @@ import * as React from 'react'; +import { Provider } from 'react-redux'; -import NewPost from './NewPost'; -import SearchFilter from './SearchFilter'; -import PostStatusFilter from './PostStatusFilter'; -import PostList from './PostList'; +import store from '../../stores'; -import IBoard from '../../interfaces/IBoard'; -import IPost from '../../interfaces/IPost'; -import IPostStatus from '../../interfaces/IPostStatus'; +import Board from '../../containers/Board'; import '../../stylesheets/components/Board.scss'; -interface Props { - board: IBoard; - isLoggedIn: boolean; - authenticityToken: string; -} +const BoardRoot = ({ board, isLoggedIn, authenticityToken }) => ( + + + +); -interface State { - filters: { - byPostStatus: number; - searchQuery: string; - }; - posts: { - items: Array; - areLoading: boolean; - error: string; - - page: number; - hasMore: boolean; - }; - postStatuses: { - items: Array; - areLoading: boolean; - error: string; - }; -} - -class Board extends React.Component { - searchFilterTimeoutId: ReturnType; - - constructor(props) { - super(props); - - this.state = { - filters: { - byPostStatus: 0, - searchQuery: '', - }, - posts: { - items: [], - areLoading: false, - error: '', - - page: 0, - hasMore: true, - }, - postStatuses: { - items: [], - areLoading: false, - error: '', - } - }; - - this.requestPosts = this.requestPosts.bind(this); - this.loadMorePosts = this.loadMorePosts.bind(this); - this.requestPostStatuses = this.requestPostStatuses.bind(this); - - this.setSearchFilter = this.setSearchFilter.bind(this); - this.setPostStatusFilter = this.setPostStatusFilter.bind(this); - } - - componentDidMount() { - this.requestPosts(); - this.requestPostStatuses(); - } - - loadMorePosts() { - this.requestPosts(this.state.posts.page + 1); - } - - async requestPosts(page = 1) { - if (this.state.posts.areLoading) return; - - this.setState({ - posts: { ...this.state.posts, areLoading: true }, - }); - - const boardId = this.props.board.id; - const { byPostStatus, searchQuery } = this.state.filters; - - let params = ''; - params += `page=${page}`; - params += `&board_id=${boardId}`; - if (byPostStatus) params += `&post_status_id=${byPostStatus}`; - if (searchQuery) params += `&search=${searchQuery}`; - - try { - let res = await fetch(`/posts?${params}`); - let data = await res.json(); - - if (page === 1) { - 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: '', - page, - hasMore: data.length === 15, - } - }); - } else { - this.setState({ - posts: { - items: [...this.state.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: '', - page, - hasMore: data.length === 15, - } - }); - } - - - } catch (e) { - this.setState({ - posts: { ...this.state.posts, error: 'An unknown error occurred, try again.' }, - }); - } - } - - setSearchFilter(searchQuery: string) { - if (this.searchFilterTimeoutId) clearInterval(this.searchFilterTimeoutId); - - this.searchFilterTimeoutId = setTimeout(() => ( - this.setState({ - filters: { ...this.state.filters, searchQuery }, - }, this.requestPosts) - ), 500); - } - - setPostStatusFilter(postStatusId: number) { - this.setState({ - filters: { ...this.state.filters, byPostStatus: postStatusId }, - }, this.requestPosts); - } - - async requestPostStatuses() { - this.setState({ - postStatuses: { ...this.state.postStatuses, areLoading: true }, - }); - - try { - let res = await fetch('/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, filters } = this.state; - - return ( -
-
- - - -
- -
- ); - } -} - -export default Board; \ No newline at end of file +export default BoardRoot; \ No newline at end of file diff --git a/app/javascript/components/Roadmap/PostList.tsx b/app/javascript/components/Roadmap/PostList.tsx index df3e024f..a7a8ac50 100644 --- a/app/javascript/components/Roadmap/PostList.tsx +++ b/app/javascript/components/Roadmap/PostList.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import PostListItem from './PostListItem'; -import IPost from '../../interfaces/IPost'; +import IPostJSON from '../../interfaces/json/IPost'; import IBoard from '../../interfaces/IBoard'; interface Props { - posts: Array; + posts: Array; boards: Array; } diff --git a/app/javascript/components/Roadmap/PostListByPostStatus.tsx b/app/javascript/components/Roadmap/PostListByPostStatus.tsx index 17d645bf..9974507b 100644 --- a/app/javascript/components/Roadmap/PostListByPostStatus.tsx +++ b/app/javascript/components/Roadmap/PostListByPostStatus.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import PostList from './PostList'; import IPostStatus from '../../interfaces/IPostStatus'; -import IPost from '../../interfaces/IPost'; +import IPostJSON from '../../interfaces/json/IPost'; import IBoard from '../../interfaces/IBoard'; interface Props { postStatus: IPostStatus; - posts: Array; + posts: Array; boards: Array; } diff --git a/app/javascript/components/Roadmap/index.tsx b/app/javascript/components/Roadmap/index.tsx index 96add6cb..0e1f5fe8 100644 --- a/app/javascript/components/Roadmap/index.tsx +++ b/app/javascript/components/Roadmap/index.tsx @@ -3,14 +3,14 @@ import * as React from 'react'; import PostListByPostStatus from './PostListByPostStatus'; import IPostStatus from '../../interfaces/IPostStatus'; -import IPost from '../../interfaces/IPost'; +import IPostJSON from '../../interfaces/json/IPost'; import IBoard from '../../interfaces/IBoard'; import '../../stylesheets/components/Roadmap.scss'; interface Props { postStatuses: Array; - posts: Array; + posts: Array; boards: Array; } diff --git a/app/javascript/containers/Board.tsx b/app/javascript/containers/Board.tsx new file mode 100644 index 00000000..da82e870 --- /dev/null +++ b/app/javascript/containers/Board.tsx @@ -0,0 +1,40 @@ +import { connect } from 'react-redux'; + +import { requestPosts } from '../actions/requestPosts'; +import { requestPostStatuses } from '../actions/requestPostStatuses'; +import { + setSearchFilter, + setPostStatusFilter, +} from '../actions/changeFilters'; + +import { State } from '../reducers/rootReducer'; + +import BoardP from '../components/Board/BoardP'; + +const mapStateToProps = (state: State) => ({ + posts: state.posts, + postStatuses: state.postStatuses, +}); + +const mapDispatchToProps = (dispatch) => ({ + requestPosts(boardId: number, page: number = 1, searchQuery: string = '', postStatusId: number) { + dispatch(requestPosts(boardId, page, searchQuery, postStatusId)); + }, + + requestPostStatuses() { + dispatch(requestPostStatuses()); + }, + + handleSearchFilterChange(searchQuery: string) { + dispatch(setSearchFilter(searchQuery)); + }, + + handlePostStatusFilterChange(postStatusId: number) { + dispatch(setPostStatusFilter(postStatusId)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(BoardP); \ No newline at end of file diff --git a/app/javascript/interfaces/IPost.ts b/app/javascript/interfaces/IPost.ts index fe6ae13b..c72d2840 100644 --- a/app/javascript/interfaces/IPost.ts +++ b/app/javascript/interfaces/IPost.ts @@ -2,13 +2,10 @@ interface IPost { id: number; title: string; description?: string; - board_id: number; - post_status_id?: number; - user_id: number; - created_at: string; - - // associations - postStatus?: any; + boardId: number; + postStatusId?: number; + userId: number; + createdAt: string; } export default IPost; \ No newline at end of file diff --git a/app/javascript/interfaces/json/IPost.ts b/app/javascript/interfaces/json/IPost.ts new file mode 100644 index 00000000..28bb227e --- /dev/null +++ b/app/javascript/interfaces/json/IPost.ts @@ -0,0 +1,11 @@ +interface IPostJSON { + id: number; + title: string; + description?: string; + board_id: number; + post_status_id?: number; + user_id: number; + created_at: string; +} + +export default IPostJSON; \ No newline at end of file diff --git a/app/javascript/reducers/filtersReducer.ts b/app/javascript/reducers/filtersReducer.ts new file mode 100644 index 00000000..03fa0860 --- /dev/null +++ b/app/javascript/reducers/filtersReducer.ts @@ -0,0 +1,39 @@ +import { + ChangeFiltersActionTypes, + SET_SEARCH_FILTER, + SET_POST_STATUS_FILTER, +} from '../actions/changeFilters'; + +export interface FiltersState { + searchQuery: string; + postStatusId: number; +} + +const initialState: FiltersState = { + searchQuery: '', + postStatusId: null, +} + +const filtersReducer = ( + state = initialState, + action: ChangeFiltersActionTypes, +) => { + switch (action.type) { + case SET_SEARCH_FILTER: + return { + ...state, + searchQuery: action.searchQuery, + }; + + case SET_POST_STATUS_FILTER: + return { + ...state, + postStatusId: action.postStatusId, + }; + + default: + return state; + } +} + +export default filtersReducer; \ No newline at end of file diff --git a/app/javascript/reducers/postReducer.ts b/app/javascript/reducers/postReducer.ts new file mode 100644 index 00000000..f7c80cc6 --- /dev/null +++ b/app/javascript/reducers/postReducer.ts @@ -0,0 +1,34 @@ +import IPost from '../interfaces/IPost'; + +const initialState: IPost = { + id: 0, + title: '', + description: null, + boardId: 0, + postStatusId: null, + userId: 0, + createdAt: '', +}; + +const postReducer = ( + state = initialState, + action, +): IPost => { + switch (action.type) { + case 'CONVERT': + return { + id: action.post.id, + title: action.post.title, + description: action.post.description, + boardId: action.post.board_id, + postStatusId: action.post.post_status_id, + userId: action.post.user_id, + createdAt: action.post.created_at, + }; + + default: + return state; + } +} + +export default postReducer; \ No newline at end of file diff --git a/app/javascript/reducers/postStatusesReducer.ts b/app/javascript/reducers/postStatusesReducer.ts new file mode 100644 index 00000000..b697f046 --- /dev/null +++ b/app/javascript/reducers/postStatusesReducer.ts @@ -0,0 +1,57 @@ +import { + PostStatusesRequestActionTypes, + POST_STATUSES_REQUEST_START, + POST_STATUSES_REQUEST_SUCCESS, + POST_STATUSES_REQUEST_FAILURE, +} from '../actions/requestPostStatuses'; + +import IPostStatus from '../interfaces/IPostStatus'; + +export interface PostStatusesState { + items: Array; + areLoading: boolean; + error: string; +} + +const initialState: PostStatusesState = { + items: [], + areLoading: false, + error: '', +} + +const postStatusesReducer = ( + state = initialState, + action: PostStatusesRequestActionTypes, +) => { + switch (action.type) { + case POST_STATUSES_REQUEST_START: + return { + ...state, + areLoading: true, + }; + + case POST_STATUSES_REQUEST_SUCCESS: + return { + ...state, + items: action.postStatuses.map(postStatus => ({ + id: postStatus.id, + name: postStatus.name, + color: postStatus.color, + })), + areLoading: false, + error: '', + }; + + case POST_STATUSES_REQUEST_FAILURE: + return { + ...state, + areLoading: false, + error: action.error, + }; + + default: + return state; + } +} + +export default postStatusesReducer; \ No newline at end of file diff --git a/app/javascript/reducers/postsReducer.ts b/app/javascript/reducers/postsReducer.ts new file mode 100644 index 00000000..9d1b1182 --- /dev/null +++ b/app/javascript/reducers/postsReducer.ts @@ -0,0 +1,85 @@ +import IPost from '../interfaces/IPost'; + +import { FiltersState } from './filtersReducer'; + +import postReducer from './postReducer'; +import filtersReducer from './filtersReducer'; + +import { + PostsRequestActionTypes, + POSTS_REQUEST_START, + POSTS_REQUEST_SUCCESS, + POSTS_REQUEST_FAILURE, +} from '../actions/requestPosts'; + +import { + ChangeFiltersActionTypes, + SET_SEARCH_FILTER, + SET_POST_STATUS_FILTER, +} from '../actions/changeFilters'; + +export interface PostsState { + items: Array; + page: number; + haveMore: boolean; + areLoading: boolean; + error: string; + filters: FiltersState; +} + +const initialState: PostsState = { + items: [], + page: 0, + haveMore: true, + areLoading: false, + error: '', + filters: { // improve + searchQuery: '', + postStatusId: null, + }, +}; + +const postsReducer = ( + state = initialState, + action: PostsRequestActionTypes | ChangeFiltersActionTypes, +): PostsState => { + switch (action.type) { + case POSTS_REQUEST_START: + return { + ...state, + areLoading: true, + }; + + case POSTS_REQUEST_SUCCESS: + return { + ...state, + items: action.page === 1 ? + action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post})) //improve + : + [...state.items, ...action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post}))], + page: action.page, + haveMore: action.posts.length === 15, + areLoading: false, + error: '', + }; + + case POSTS_REQUEST_FAILURE: + return { + ...state, + areLoading: false, + error: action.error, + }; + + case SET_SEARCH_FILTER: + case SET_POST_STATUS_FILTER: + return { + ...state, + filters: filtersReducer(state.filters, action), + }; + + default: + return state; + } +} + +export default postsReducer; \ No newline at end of file diff --git a/app/javascript/reducers/rootReducer.ts b/app/javascript/reducers/rootReducer.ts new file mode 100644 index 00000000..fb615a2e --- /dev/null +++ b/app/javascript/reducers/rootReducer.ts @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux'; + +import postsReducer from './postsReducer'; +import postStatusesReducer from './postStatusesReducer'; + +const rootReducer = combineReducers({ + posts: postsReducer, + postStatuses: postStatusesReducer, +}); + +export type State = ReturnType +export default rootReducer; \ No newline at end of file diff --git a/app/javascript/stores/index.ts b/app/javascript/stores/index.ts new file mode 100644 index 00000000..bfd9c99e --- /dev/null +++ b/app/javascript/stores/index.ts @@ -0,0 +1,15 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunkMiddleware from 'redux-thunk'; + +import rootReducer from '../reducers/rootReducer'; + +const store = createStore( + rootReducer, + applyMiddleware( + thunkMiddleware, + ), +); + +store.subscribe(() => console.log(store.getState())); + +export default store; \ No newline at end of file diff --git a/package.json b/package.json index ee582d1e..240a283c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "react": "^16.9.0", "react-dom": "^16.9.0", "react-infinite-scroller": "^1.2.4", + "react-redux": "^7.1.1", "react_ujs": "^2.6.0", + "redux": "^4.0.4", + "redux-thunk": "^2.3.0", "ts-loader": "^6.0.4", "turbolinks": "^5.2.0", "typescript": "^3.5.3" diff --git a/yarn.lock b/yarn.lock index ce12d4f5..ef317936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -700,6 +700,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.5.5": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" + integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.1.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -3212,6 +3219,13 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" + integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -3463,7 +3477,7 @@ interpret@1.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -invariant@^2.2.2: +invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -5812,11 +5826,23 @@ react-infinite-scroller@^1.2.4: dependencies: prop-types "^15.5.8" -react-is@^16.8.1: +react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== +react-redux@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.1.tgz#ce6eee1b734a7a76e0788b3309bf78ff6b34fa0a" + integrity sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react@^16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa" @@ -5896,6 +5922,19 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -6708,6 +6747,11 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"