Files
btrc-hub/frontend/src/pages/posts/PostListPage.tsx
T
2025-09-04 23:55:09 +09:00

145 lines
4.0 KiB
TypeScript

import axios from 'axios'
import toCamel from 'camelcase-keys'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useNavigationType } from 'react-router-dom'
import PostList from '@/components/PostList'
import TagSidebar from '@/components/TagSidebar'
import WikiBody from '@/components/WikiBody'
import TabGroup, { Tab } from '@/components/common/TabGroup'
import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config'
import type { Post, WikiPage } from '@/types'
export default () => {
const navigationType = useNavigationType ()
const containerRef = useRef<HTMLDivElement | null> (null)
const loaderRef = useRef<HTMLDivElement | null> (null)
const [cursor, setCursor] = useState ('')
const [loading, setLoading] = useState (false)
const [posts, setPosts] = useState<Post[]> ([])
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const loadMore = async (withCursor: boolean) => {
setLoading (true)
const res = await axios.get (`${ API_BASE_URL }/posts`, {
params: { tags: tags.join (' '),
match: anyFlg ? 'any' : 'all',
limit: '20',
...(withCursor && { cursor }) } })
const data = toCamel (res.data as any, { deep: true }) as { posts: Post[]
nextCursor: string }
setPosts (posts => (
[...((new Map ([...(withCursor ? posts : []), ...data.posts]
.map (post => [post.id, post])))
.values ())]))
setCursor (data.nextCursor)
setLoading (false)
}
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
const anyFlg = query.get ('match') === 'any'
const tags = tagsQuery.split (' ').filter (e => e !== '')
useEffect(() => {
const observer = new IntersectionObserver (entries => {
if (entries[0].isIntersecting && !(loading) && cursor)
loadMore (true)
}, { threshold: 1 })
const target = loaderRef.current
target && observer.observe (target)
return () => {
target && observer.unobserve (target)
}
}, [loaderRef, loading])
useLayoutEffect (() => {
const savedState = sessionStorage.getItem (`posts:${ tagsQuery }`)
if (savedState && navigationType === 'POP')
{
const { posts, cursor, scroll } = JSON.parse (savedState)
setPosts (posts)
setCursor (cursor)
if (containerRef.current)
containerRef.current.scrollTop = scroll
loadMore (true)
}
else
{
setPosts ([])
loadMore (false)
}
setWikiPage (null)
if (tags.length === 1)
{
void (async () => {
try
{
const tagName = tags[0]
const res = await axios.get (`${ API_BASE_URL }/wiki/title/${ tagName }`)
setWikiPage (toCamel (res.data as any, { deep: true }) as WikiPage)
}
catch
{
;
}
}) ()
}
}, [location.search])
return (
<div className="md:flex md:flex-1" ref={containerRef}>
<Helmet>
<title>
{tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
: `${ SITE_TITLE } 〜 ぼざろクリーチャーシリーズ綜合リンク集サイト`}
</title>
</Helmet>
<TagSidebar posts={posts.slice (0, 20)}/>
<MainArea>
<TabGroup>
<Tab name="広場">
{posts.length
? (
<PostList posts={posts} onClick={() => {
const statesToSave = {
posts, cursor,
scroll: containerRef.current?.scrollTop ?? 0 }
sessionStorage.setItem (`posts:${ tagsQuery }`,
JSON.stringify (statesToSave))
}}/>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
<div ref={loaderRef} className="h-12"/>
</Tab>
{tags.length === 1 && (
<Tab name="Wiki">
<WikiBody title={tags[0]} body={wikiPage?.body}/>
<div className="my-2">
<Link to={`/wiki/${ encodeURIComponent (tags[0]) }`}>
Wiki
</Link>
</div>
</Tab>)}
</TabGroup>
</MainArea>
</div>)
}