diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 96af1c1..ce555ed 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -3,20 +3,35 @@ class PostsController < ApplicationController # GET /posts def index - limit = params[:limit].presence&.to_i + page = (params[:page].presence || 1).to_i + limit = (params[:limit].presence || 20).to_i cursor = params[:cursor].presence - created_at = ('COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' + - 'posts.original_created_from,' + - 'posts.created_at)') - q = filtered_posts.order(Arel.sql("#{ created_at } DESC")) - q = q.where("#{ created_at } < ?", Time.iso8601(cursor)) if cursor + page = 1 if page < 1 + limit = 1 if limit < 1 + + offset = (page - 1) * limit - posts = limit ? q.limit(limit + 1) : q + sort_sql = + 'COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' + + 'posts.original_created_from,' + + 'posts.created_at)' + q = + filtered_posts + .preload(:tags) + .with_attached_thumbnail + .select("posts.*, #{ sort_sql } AS sort_ts") + .order(Arel.sql("#{ sort_sql } DESC")) + posts = ( + if cursor + q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1) + else + q.limit(limit).offset(offset) + end).to_a next_cursor = nil - if limit && posts.size > limit - next_cursor = posts.last.created_at.iso8601(6) + if cursor && posts.length > limit + next_cursor = posts.last.read_attribute('sort_ts').iso8601(6) posts = posts.first(limit) end @@ -29,7 +44,7 @@ class PostsController < ApplicationController nil end end - }, next_cursor: } + }, count: filtered_posts.count(:id), next_cursor: } end def random diff --git a/frontend/src/pages/posts/PostListPage.tsx b/frontend/src/pages/posts/PostListPage.tsx index fdda686..50cc04f 100644 --- a/frontend/src/pages/posts/PostListPage.tsx +++ b/frontend/src/pages/posts/PostListPage.tsx @@ -7,6 +7,7 @@ import { Link, useLocation, useNavigationType } from 'react-router-dom' import PostList from '@/components/PostList' import TagSidebar from '@/components/TagSidebar' import WikiBody from '@/components/WikiBody' +import Pagination from '@/components/common/Pagination' import TabGroup, { Tab } from '@/components/common/TabGroup' import MainArea from '@/components/layout/MainArea' import { API_BASE_URL, SITE_TITLE } from '@/config' @@ -23,6 +24,7 @@ export default () => { const [cursor, setCursor] = useState ('') const [loading, setLoading] = useState (false) const [posts, setPosts] = useState ([]) + const [totalPages, setTotalPages] = useState (0) const [wikiPage, setWikiPage] = useState (null) const loadMore = async (withCursor: boolean) => { @@ -31,15 +33,19 @@ export default () => { const res = await axios.get (`${ API_BASE_URL }/posts`, { params: { tags: tags.join (' '), match: anyFlg ? 'any' : 'all', - limit: '20', + ...(page && { page }), + ...(limit && { limit }), ...(withCursor && { cursor }) } }) - const data = toCamel (res.data as any, { deep: true }) as { posts: Post[] - nextCursor: string } + const data = toCamel (res.data as any, { deep: true }) as { + posts: Post[] + count: number + nextCursor: string } setPosts (posts => ( [...((new Map ([...(withCursor ? posts : []), ...data.posts] .map (post => [post.id, post]))) .values ())])) setCursor (data.nextCursor) + setTotalPages (Math.ceil (data.count / limit)) setLoading (false) } @@ -49,6 +55,8 @@ export default () => { const tagsQuery = query.get ('tags') ?? '' const anyFlg = query.get ('match') === 'any' const tags = tagsQuery.split (' ').filter (e => e !== '') + const page = Number (query.get ('page') ?? 1) + const limit = Number (query.get ('limit') ?? 20) useEffect(() => { const observer = new IntersectionObserver (entries => { @@ -65,7 +73,8 @@ export default () => { }, [loaderRef, loading]) useLayoutEffect (() => { - const savedState = sessionStorage.getItem (`posts:${ tagsQuery }`) + // TODO: 無限ロード用 + const savedState = /* sessionStorage.getItem (`posts:${ tagsQuery }`) */ null if (savedState && navigationType === 'POP') { const { posts, cursor, scroll } = JSON.parse (savedState) @@ -116,18 +125,23 @@ export default () => { - {posts.length + {posts.length > 0 ? ( - { - const statesToSave = { - posts, cursor, - scroll: containerRef.current?.scrollTop ?? 0 } - sessionStorage.setItem (`posts:${ tagsQuery }`, - JSON.stringify (statesToSave)) - }}/>) + <> + { + // TODO: 無限ロード用なので復活時に戻す. + // const statesToSave = { + // posts, cursor, + // scroll: containerRef.current?.scrollTop ?? 0 } + // sessionStorage.setItem (`posts:${ tagsQuery }`, + // JSON.stringify (statesToSave)) + }}/> + + ) : !(loading) && '広場には何もありませんよ.'} {loading && 'Loading...'} -
+ {/* TODO: 無限ローディング復活までコメント・アウト */} + {/*
*/} {tags.length === 1 && (