ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

159 lines
4.6 KiB

  1. import axios from 'axios'
  2. import toCamel from 'camelcase-keys'
  3. import { useEffect, useLayoutEffect, useRef, useState } from 'react'
  4. import { Helmet } from 'react-helmet-async'
  5. import { Link, useLocation, useNavigationType } from 'react-router-dom'
  6. import PostList from '@/components/PostList'
  7. import TagSidebar from '@/components/TagSidebar'
  8. import WikiBody from '@/components/WikiBody'
  9. import Pagination from '@/components/common/Pagination'
  10. import TabGroup, { Tab } from '@/components/common/TabGroup'
  11. import MainArea from '@/components/layout/MainArea'
  12. import { API_BASE_URL, SITE_TITLE } from '@/config'
  13. import type { Post, WikiPage } from '@/types'
  14. export default () => {
  15. const navigationType = useNavigationType ()
  16. const containerRef = useRef<HTMLDivElement | null> (null)
  17. const loaderRef = useRef<HTMLDivElement | null> (null)
  18. const [cursor, setCursor] = useState ('')
  19. const [loading, setLoading] = useState (false)
  20. const [posts, setPosts] = useState<Post[]> ([])
  21. const [totalPages, setTotalPages] = useState (0)
  22. const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
  23. const loadMore = async (withCursor: boolean) => {
  24. setLoading (true)
  25. const res = await axios.get (`${ API_BASE_URL }/posts`, {
  26. params: { tags: tags.join (' '),
  27. match: anyFlg ? 'any' : 'all',
  28. ...(page && { page }),
  29. ...(limit && { limit }),
  30. ...(withCursor && { cursor }) } })
  31. const data = toCamel (res.data as any, { deep: true }) as {
  32. posts: Post[]
  33. count: number
  34. nextCursor: string }
  35. setPosts (posts => (
  36. [...((new Map ([...(withCursor ? posts : []), ...data.posts]
  37. .map (post => [post.id, post])))
  38. .values ())]))
  39. setCursor (data.nextCursor)
  40. setTotalPages (Math.ceil (data.count / limit))
  41. setLoading (false)
  42. }
  43. const location = useLocation ()
  44. const query = new URLSearchParams (location.search)
  45. const tagsQuery = query.get ('tags') ?? ''
  46. const anyFlg = query.get ('match') === 'any'
  47. const tags = tagsQuery.split (' ').filter (e => e !== '')
  48. const page = Number (query.get ('page') ?? 1)
  49. const limit = Number (query.get ('limit') ?? 20)
  50. useEffect(() => {
  51. const observer = new IntersectionObserver (entries => {
  52. if (entries[0].isIntersecting && !(loading) && cursor)
  53. loadMore (true)
  54. }, { threshold: 1 })
  55. const target = loaderRef.current
  56. target && observer.observe (target)
  57. return () => {
  58. target && observer.unobserve (target)
  59. }
  60. }, [loaderRef, loading])
  61. useLayoutEffect (() => {
  62. // TODO: 無限ロード用
  63. const savedState = /* sessionStorage.getItem (`posts:${ tagsQuery }`) */ null
  64. if (savedState && navigationType === 'POP')
  65. {
  66. const { posts, cursor, scroll } = JSON.parse (savedState)
  67. setPosts (posts)
  68. setCursor (cursor)
  69. if (containerRef.current)
  70. containerRef.current.scrollTop = scroll
  71. loadMore (true)
  72. }
  73. else
  74. {
  75. setPosts ([])
  76. loadMore (false)
  77. }
  78. setWikiPage (null)
  79. if (tags.length === 1)
  80. {
  81. void (async () => {
  82. try
  83. {
  84. const tagName = tags[0]
  85. const res = await axios.get (`${ API_BASE_URL }/wiki/title/${ tagName }`)
  86. setWikiPage (toCamel (res.data as any, { deep: true }) as WikiPage)
  87. }
  88. catch
  89. {
  90. ;
  91. }
  92. }) ()
  93. }
  94. }, [location.search])
  95. return (
  96. <div className="md:flex md:flex-1" ref={containerRef}>
  97. <Helmet>
  98. <title>
  99. {tags.length
  100. ? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
  101. : `${ SITE_TITLE } 〜 ぼざろクリーチャーシリーズ綜合リンク集サイト`}
  102. </title>
  103. </Helmet>
  104. <TagSidebar posts={posts.slice (0, 20)}/>
  105. <MainArea>
  106. <TabGroup>
  107. <Tab name="広場">
  108. {posts.length > 0
  109. ? (
  110. <>
  111. <PostList posts={posts} onClick={() => {
  112. // TODO: 無限ロード用なので復活時に戻す.
  113. // const statesToSave = {
  114. // posts, cursor,
  115. // scroll: containerRef.current?.scrollTop ?? 0 }
  116. // sessionStorage.setItem (`posts:${ tagsQuery }`,
  117. // JSON.stringify (statesToSave))
  118. }}/>
  119. <Pagination page={page} totalPages={totalPages}/>
  120. </>)
  121. : !(loading) && '広場には何もありませんよ.'}
  122. {loading && 'Loading...'}
  123. {/* TODO: 無限ローディング復活までコメント・アウト */}
  124. {/* <div ref={loaderRef} className="h-12"/> */}
  125. </Tab>
  126. {tags.length === 1 && (
  127. <Tab name="Wiki">
  128. <WikiBody title={tags[0]} body={wikiPage?.body}/>
  129. <div className="my-2">
  130. <Link to={`/wiki/${ encodeURIComponent (tags[0]) }`}>
  131. Wiki を見る
  132. </Link>
  133. </div>
  134. </Tab>)}
  135. </TabGroup>
  136. </MainArea>
  137. </div>)
  138. }