diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb
index 70eb22f..00618a1 100644
--- a/backend/app/controllers/posts_controller.rb
+++ b/backend/app/controllers/posts_controller.rb
@@ -5,16 +5,26 @@ require 'nokogiri'
class PostsController < ApplicationController
# GET /posts
def index
- latest_id = params[:latest].to_i
- offset = params[:offset].to_i
-
- posts = filtered_posts.order(created_at: :desc)
- posts = if posts.first&.id == latest_id
- posts.limit(20).offset(offset)
- else
- posts.limit(offset + 20)
- end
- render json: posts.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
+ limit = (params[:limit] || 20).to_i
+ cursor = params[:cursor]
+
+ q = filtered_posts.order(created_at: :desc)
+
+ next_cursor = nil
+ if cursor.present?
+ q = q.where('posts.created_at < ?', Time.iso8601(cursor))
+ end
+
+ posts = q.limit(limit + 1)
+
+ next_cursor = nil
+ if posts.size > limit
+ next_cursor = posts.last.created_at.iso8601(6)
+ posts = posts.first(limit)
+ end
+
+ render json: { posts: posts.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
+ next_cursor: }
end
def random
@@ -31,7 +41,10 @@ class PostsController < ApplicationController
# GET /posts/1
def show
post = Post.includes(:tags).find(params[:id])
- viewed = current_user&.viewed?(post)
+ return head :not_found unless post
+
+ viewed = current_user&.viewed?(post) || false
+
render json: (post
.as_json(include: { tags: { only: [:id, :name, :category] } })
.merge(viewed: viewed))
@@ -95,7 +108,7 @@ class PostsController < ApplicationController
private
def filtered_posts
- tag_names = params[:tags]&.split(',')
+ tag_names = params[:tags]&.split(?\ )
match_type = params[:match]
tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all
end
diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx
index aaf9aea..e0c2f63 100644
--- a/frontend/src/components/TagSidebar.tsx
+++ b/frontend/src/components/TagSidebar.tsx
@@ -7,6 +7,7 @@ import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
import { API_BASE_URL } from '@/config'
+import { CATEGORIES } from '@/consts'
import type { Post, Tag } from '@/types'
@@ -27,6 +28,8 @@ export default ({ posts }: Props) => {
useEffect (() => {
const tagsTmp: TagByCategory = { }
+ let cnt = 0
+ loop:
for (const post of posts)
{
for (const tag of post.tags)
@@ -34,7 +37,12 @@ export default ({ posts }: Props) => {
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
- tagsTmp[tag.category].push (tag)
+ {
+ tagsTmp[tag.category].push (tag)
+ ++cnt
+ if (cnt >= 25)
+ break loop
+ }
}
}
for (const cat of Object.keys (tagsTmp))
@@ -47,7 +55,7 @@ export default ({ posts }: Props) => {
タグ
- {['general', 'deerjikist', 'nico'].map (cat => cat in tags && (
+ {CATEGORIES.map (cat => cat in tags && (
<>
{tags[cat].map (tag => (
-
diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx
index 2cf6e24..74dafc4 100644
--- a/frontend/src/pages/posts/PostDetailPage.tsx
+++ b/frontend/src/pages/posts/PostDetailPage.tsx
@@ -1,4 +1,5 @@
import axios from 'axios'
+import toCamel from 'camelcase-keys'
import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Link, useLocation, useParams } from 'react-router-dom'
@@ -32,9 +33,9 @@ export default ({ user }: Props) => {
void (axios.delete (
`${ API_BASE_URL }/posts/${ id }/viewed`,
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
- .then (res => setPost (post => ({ ...post, viewed: false })))
- .catch (err => toast ({ title: '失敗……',
- description: '通信に失敗しました……' })))
+ .then (() => setPost (post => ({ ...post, viewed: false })))
+ .catch (() => toast ({ title: '失敗……',
+ description: '通信に失敗しました……' })))
}
else
{
@@ -42,9 +43,9 @@ export default ({ user }: Props) => {
`${ API_BASE_URL }/posts/${ id }/viewed`,
{ },
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
- .then (res => setPost (post => ({ ...post, viewed: true })))
- .catch (err => toast ({ title: '失敗……',
- description: '通信に失敗しました……' })))
+ .then (() => setPost (post => ({ ...post, viewed: true })))
+ .catch (() => toast ({ title: '失敗……',
+ description: '通信に失敗しました……' })))
}
}
@@ -53,7 +54,7 @@ export default ({ user }: Props) => {
return
void (axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
- .then (res => setPost (res.data))
+ .then (res => setPost (toCamel (res.data, { deep: true })))
.catch (err => console.error ('うんち!', err)))
}, [id])
@@ -78,6 +79,8 @@ export default ({ user }: Props) => {
return (
<>
+ {(post?.thumbnail || post?.thumbnailBase) &&
+ }
{post && {`${ post.title || post.url } | ${ SITE_TITLE }`}}
diff --git a/frontend/src/pages/posts/PostListPage.tsx b/frontend/src/pages/posts/PostListPage.tsx
index 975b61e..97cf00a 100644
--- a/frontend/src/pages/posts/PostListPage.tsx
+++ b/frontend/src/pages/posts/PostListPage.tsx
@@ -1,6 +1,6 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Link, useLocation } from 'react-router-dom'
@@ -16,6 +16,22 @@ import type { Post, Tag, WikiPage } from '@/types'
export default () => {
const [posts, setPosts] = useState (null)
const [wikiPage, setWikiPage] = useState (null)
+ const [cursor, setCursor] = useState ('')
+ const [loading, setLoading] = useState (false)
+
+ const loaderRef = useRef (null)
+
+ const loadMore = async () => {
+ setLoading (true)
+ const res = await axios.get (`${ API_BASE_URL }/posts`, {
+ params: { tags: tags.join (' '),
+ match: (anyFlg ? 'any' : 'all'),
+ cursor } })
+ const data = toCamel (res.data, { deep: true })
+ setPosts (posts => [...(posts || []), ...data.posts])
+ setCursor (data.nextCursor)
+ setLoading (false)
+ }
const location = useLocation ()
const query = new URLSearchParams (location.search)
@@ -24,14 +40,22 @@ export default () => {
const tags = tagsQuery.split (' ').filter (e => e !== '')
useEffect(() => {
- void (axios.get (`${ API_BASE_URL }/posts`, {
- params: { tags: tags.join (','),
- match: (anyFlg ? 'any' : 'all') } })
- .then (res => setPosts (toCamel (res.data, { deep: true })))
- .catch (err => {
- console.error ('Failed to fetch posts:', err)
- setPosts ([])
- }))
+ const observer = new IntersectionObserver (entries => {
+ if (entries[0].isIntersecting && !(loading) && cursor)
+ loadMore ()
+ }, { threshold: 1 })
+
+ const target = loaderRef.current
+ target && observer.observe (target)
+
+ return () => target && observer.unobserve (target)
+ }, [loaderRef, loading])
+
+ useEffect (() => {
+ console.log ('test')
+ setCursor ('')
+ setPosts (null)
+ loadMore ()
setWikiPage (null)
if (tags.length === 1)
@@ -50,10 +74,10 @@ export default () => {
{tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
- : `${ SITE_TITLE } 〜 ぼざクリも、ぼざろ外も、外伝もあるんだよ`}
+ : `${ SITE_TITLE } 〜 ぼざクリ関聯綜合リンク集サイト`}
-
+
@@ -66,10 +90,12 @@ export default () => {
key={post.id}
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
))}
)
: '広場には何もありませんよ.')}
+
{(wikiPage && wikiPage.body) && (