Browse Source

feat: 広場のページネーション(#169) (#189)

#169 バグ修正

Merge remote-tracking branch 'origin/main' into feature/169

#169

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/189
feature/157
みてるぞ 1 week ago
parent
commit
d224ccef4c
2 changed files with 52 additions and 23 deletions
  1. +25
    -10
      backend/app/controllers/posts_controller.rb
  2. +27
    -13
      frontend/src/pages/posts/PostListPage.tsx

+ 25
- 10
backend/app/controllers/posts_controller.rb View File

@@ -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


+ 27
- 13
frontend/src/pages/posts/PostListPage.tsx View File

@@ -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<Post[]> ([])
const [totalPages, setTotalPages] = useState (0)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (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 () => {
<MainArea>
<TabGroup>
<Tab name="広場">
{posts.length
{posts.length > 0
? (
<PostList posts={posts} onClick={() => {
const statesToSave = {
posts, cursor,
scroll: containerRef.current?.scrollTop ?? 0 }
sessionStorage.setItem (`posts:${ tagsQuery }`,
JSON.stringify (statesToSave))
}}/>)
<>
<PostList posts={posts} onClick={() => {
// TODO: 無限ロード用なので復活時に戻す.
// const statesToSave = {
// posts, cursor,
// scroll: containerRef.current?.scrollTop ?? 0 }
// sessionStorage.setItem (`posts:${ tagsQuery }`,
// JSON.stringify (statesToSave))
}}/>
<Pagination page={page} totalPages={totalPages}/>
</>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
<div ref={loaderRef} className="h-12"/>
{/* TODO: 無限ローディング復活までコメント・アウト */}
{/* <div ref={loaderRef} className="h-12"/> */}
</Tab>
{tags.length === 1 && (
<Tab name="Wiki">


Loading…
Cancel
Save