diff --git a/Gemfile b/Gemfile index de7aa491..87daefe0 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,9 @@ gem "administrate", git: "https://github.com/thoughtbot/administrate.git" # React gem 'react-rails' +# Pagination +gem 'kaminari' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index c3673747..a8fe8723 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -309,6 +309,7 @@ DEPENDENCIES devise! factory_bot_rails jbuilder (~> 2.7) + kaminari listen (>= 3.0.5, < 3.2) pg (>= 0.18, < 2.0) puma (~> 3.11) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index d3432bb1..21830286 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -6,6 +6,7 @@ class PostsController < ApplicationController .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(filter_params) + .page(params[:page]) render json: posts end @@ -25,11 +26,12 @@ class PostsController < ApplicationController private def filter_params - defaults = { board_id: Board.first.id } + defaults = { board_id: Board.first.id, page: 1 } params - .permit(:board_id, :post_status_id) + .permit(:board_id, :post_status_id, :page) .with_defaults(defaults) + .except(:page) # do not return page param end def post_params diff --git a/app/javascript/components/Board/PostList.tsx b/app/javascript/components/Board/PostList.tsx index 06777f07..0be18cbd 100644 --- a/app/javascript/components/Board/PostList.tsx +++ b/app/javascript/components/Board/PostList.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import InfiniteScroll from 'react-infinite-scroller'; + import PostListItem from './PostListItem'; import Spinner from '../shared/Spinner'; @@ -9,14 +11,24 @@ interface Props { posts: Array; areLoading: boolean; error: string; + + handleLoadMore(): void; + page: number; + hasMore: boolean; } -const PostList = ({ posts, areLoading, error }: Props) => ( +const PostList = ({ posts, areLoading, error, handleLoadMore, page, hasMore }: Props) => (
- { areLoading ? : null } { error ? {error} : null } - { - posts.map((post, i) => ( + } + useWindow={true} + > + {posts.map((post, i) => ( ( key={i} /> - )) - } + ))} +
); diff --git a/app/javascript/components/Board/index.tsx b/app/javascript/components/Board/index.tsx index d4f0438f..987e7645 100644 --- a/app/javascript/components/Board/index.tsx +++ b/app/javascript/components/Board/index.tsx @@ -24,6 +24,9 @@ interface State { items: Array; areLoading: boolean; error: string; + + page: number; + hasMore: boolean; }; postStatuses: { items: Array; @@ -42,19 +45,24 @@ class Board extends React.Component { }, posts: { items: [], - areLoading: true, + areLoading: false, error: '', + + page: 0, + hasMore: true, }, postStatuses: { items: [], - areLoading: true, + areLoading: false, error: '', } }; this.requestPosts = this.requestPosts.bind(this); + this.loadMorePosts = this.loadMorePosts.bind(this); this.requestPostStatuses = this.requestPostStatuses.bind(this); this.setPostStatusFilter = this.setPostStatusFilter.bind(this); + } componentDidMount() { @@ -62,7 +70,13 @@ class Board extends React.Component { this.requestPostStatuses(); } - async requestPosts() { + 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 }, }); @@ -71,26 +85,51 @@ class Board extends React.Component { const { byPostStatus } = this.state.filters; let params = ''; + params += `page=${page}`; + params += `&board_id=${boardId}`; if (byPostStatus) params += `&post_status_id=${byPostStatus}`; try { - let res = await fetch(`/posts?board_id=${boardId}${params}`); + let res = await fetch(`/posts?${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: '', - } - }); + 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.' }, @@ -156,6 +195,10 @@ class Board extends React.Component { posts={posts.items} areLoading={posts.areLoading} error={posts.error} + + handleLoadMore={this.loadMorePosts} + page={posts.page} + hasMore={posts.hasMore} /> ); diff --git a/app/models/post.rb b/app/models/post.rb index 3ffb8d89..df0ab44f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -4,4 +4,6 @@ class Post < ApplicationRecord belongs_to :post_status, optional: true validates :title, presence: true, length: { in: 4..64 } + + paginates_per 15 end diff --git a/package.json b/package.json index cb1c44f9..ee582d1e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "prop-types": "^15.7.2", "react": "^16.9.0", "react-dom": "^16.9.0", + "react-infinite-scroller": "^1.2.4", "react_ujs": "^2.6.0", "ts-loader": "^6.0.4", "turbolinks": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index 4916749e..ce12d4f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5633,7 +5633,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5805,6 +5805,13 @@ react-dom@^16.9.0: prop-types "^15.6.2" scheduler "^0.15.0" +react-infinite-scroller@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9" + integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw== + dependencies: + prop-types "^15.5.8" + react-is@^16.8.1: version "16.9.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"