ぼざクリタグ広場 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.
 
 
 
 
 

287 lines
8.5 KiB

  1. import { useQuery, useQueryClient } from '@tanstack/react-query'
  2. import { motion } from 'framer-motion'
  3. import { useEffect } from 'react'
  4. import { Helmet } from 'react-helmet-async'
  5. import { useLocation } from 'react-router-dom'
  6. import TagLink from '@/components/TagLink'
  7. import PrefetchLink from '@/components/PrefetchLink'
  8. import PageTitle from '@/components/common/PageTitle'
  9. import Pagination from '@/components/common/Pagination'
  10. import MainArea from '@/components/layout/MainArea'
  11. import { toast } from '@/components/ui/use-toast'
  12. import { SITE_TITLE } from '@/config'
  13. import { apiPut } from '@/lib/api'
  14. import { fetchPostChanges } from '@/lib/posts'
  15. import { postsKeys, tagsKeys } from '@/lib/queryKeys'
  16. import { fetchTag } from '@/lib/tags'
  17. import { cn, dateString, originalCreatedAtString } from '@/lib/utils'
  18. import type { FC } from 'react'
  19. const renderDiff = (diff: { current: string | null; prev: string | null }) => (
  20. <>
  21. {(diff.prev && diff.prev !== diff.current) && (
  22. <>
  23. <del className="text-red-600 dark:text-red-400">
  24. {diff.prev}
  25. </del>
  26. {diff.current && <br/>}
  27. </>)}
  28. {diff.current}
  29. </>)
  30. export default (() => {
  31. const location = useLocation ()
  32. const query = new URLSearchParams (location.search)
  33. const id = query.get ('id')
  34. const tagId = query.get ('tag')
  35. const page = Number (query.get ('page') ?? 1)
  36. const limit = Number (query.get ('limit') ?? 20)
  37. // 投稿列の結合で使用
  38. let rowsCnt: number
  39. const { data: tag } =
  40. tagId
  41. ? useQuery ({ queryKey: tagsKeys.show (tagId),
  42. queryFn: () => fetchTag (tagId) })
  43. : { data: null }
  44. const { data, isLoading: loading } = useQuery ({
  45. queryKey: postsKeys.changes ({ ...(id && { post: id }),
  46. ...(tagId && { tag: tagId }),
  47. page, limit }),
  48. queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
  49. ...(tagId && { tag: tagId }),
  50. page, limit }) })
  51. const changes = data?.versions ?? []
  52. const totalPages = data ? Math.ceil (data.count / limit) : 0
  53. const qc = useQueryClient ()
  54. useEffect (() => {
  55. document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
  56. }, [location.search])
  57. const layoutIds: string[] = []
  58. return (
  59. <MainArea>
  60. <Helmet>
  61. <title>{`耕作履歴 | ${ SITE_TITLE }`}</title>
  62. </Helmet>
  63. <PageTitle>
  64. 耕作履歴
  65. {id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
  66. {tag && <>(<TagLink tag={tag} withWiki={false} withCount={false}/>)</>}
  67. </PageTitle>
  68. {loading ? 'Loading...' : (
  69. <>
  70. <div className="overflow-x-auto">
  71. <table className="w-full min-w-[1200px] table-fixed border-collapse">
  72. <colgroup>
  73. {/* 投稿 */}
  74. <col className="w-64"/>
  75. {/* 版 */}
  76. <col className="w-40"/>
  77. {/* タイトル */}
  78. <col className="w-96"/>
  79. {/* URL */}
  80. <col className="w-96"/>
  81. {/* タグ */}
  82. <col className="w-[48rem]"/>
  83. {/* TODO: 親投稿 */}
  84. {/* <col className="w-[48rem]"/> */}
  85. {/* オリジナルの投稿日時 */}
  86. <col className="w-96"/>
  87. {/* 更新日時 */}
  88. <col className="w-64"/>
  89. {/* (差戻ボタン) */}
  90. <col className="w-20"/>
  91. </colgroup>
  92. <thead className="border-b-2 border-black dark:border-white">
  93. <tr>
  94. <th className="p-2 text-left">投稿</th>
  95. <th className="p-2 text-left">版</th>
  96. <th className="p-2 text-left">タイトル</th>
  97. <th className="p-2 text-left">URL</th>
  98. <th className="p-2 text-left">タグ</th>
  99. {/* TODO: 親投稿の履歴 */}
  100. {/* <th className="p-2 text-left">親投稿</th> */}
  101. <th className="p-2 text-left">オリジナルの投稿日時</th>
  102. <th className="p-2 text-left">更新日時</th>
  103. <th className="p-2"/>
  104. </tr>
  105. </thead>
  106. <tbody>
  107. {changes.map ((change, i) => {
  108. const withPost = i === 0 || change.postId !== changes[i - 1].postId
  109. if (withPost)
  110. {
  111. rowsCnt = 1
  112. for (let j = i + 1;
  113. (j < changes.length
  114. && change.postId === changes[j].postId);
  115. ++j)
  116. ++rowsCnt
  117. }
  118. let layoutId: string | undefined = `page-${ change.postId }`
  119. if (layoutIds.includes (layoutId))
  120. layoutId = undefined
  121. else
  122. layoutIds.push (layoutId)
  123. return (
  124. <tr key={`${ change.postId }.${ change.versionNo }`}
  125. className={cn ('even:bg-gray-100 dark:even:bg-gray-700',
  126. withPost && 'border-t')}>
  127. {withPost && (
  128. <td className="align-top p-2 bg-white dark:bg-[#242424] border-r"
  129. rowSpan={rowsCnt}>
  130. <PrefetchLink to={`/posts/${ change.postId }`}>
  131. <motion.div
  132. layoutId={layoutId}
  133. transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
  134. <img src={change.thumbnail.current
  135. || change.thumbnailBase.current
  136. || undefined}
  137. alt={change.title.current || change.url.current}
  138. title={change.title.current || change.url.current || undefined}
  139. className="w-40"/>
  140. </motion.div>
  141. </PrefetchLink>
  142. </td>)}
  143. <td className="p-2">{change.postId}.{change.versionNo}</td>
  144. <td className="p-2 break-all">{renderDiff (change.title)}</td>
  145. <td className="p-2 break-all">{renderDiff (change.url)}</td>
  146. <td className="p-2">
  147. {change.tags.map ((tag, i) => (
  148. tag.type === 'added'
  149. ? (
  150. <ins
  151. key={i}
  152. className="mr-2 text-green-600 dark:text-green-400">
  153. {tag.name}
  154. </ins>)
  155. : (
  156. tag.type === 'removed'
  157. ? (
  158. <del
  159. key={i}
  160. className="mr-2 text-red-600 dark:text-red-400">
  161. {tag.name}
  162. </del>)
  163. : (
  164. <span key={i} className="mr-2">
  165. {tag.name}
  166. </span>))))}
  167. </td>
  168. {/* TODO: 親投稿の履歴 */}
  169. {/* <td className="p-2">
  170. {change.parentPosts.map ((pp, i) => (
  171. pp.type === 'added'
  172. ? (
  173. <ins
  174. key={i}
  175. className="mr-2 text-green-600 dark:text-green-400">
  176. {pp.title}
  177. </ins>)
  178. : (
  179. pp.type === 'removed'
  180. ? (
  181. <del
  182. key={i}
  183. className="mr-2 text-red-600 dark:text-red-400">
  184. {pp.title}
  185. </del>)
  186. : (
  187. <span key={i} className="mr-2">
  188. {pp.title}
  189. </span>))))}
  190. </td> */}
  191. <td className="p-2">
  192. {change.versionNo === 1
  193. ? originalCreatedAtString (change.originalCreatedFrom.current,
  194. change.originalCreatedBefore.current)
  195. : renderDiff ({
  196. current: originalCreatedAtString (
  197. change.originalCreatedFrom.current,
  198. change.originalCreatedBefore.current),
  199. prev: originalCreatedAtString (
  200. change.originalCreatedFrom.prev,
  201. change.originalCreatedBefore.prev) })}
  202. </td>
  203. <td className="p-2">
  204. {change.createdByUser
  205. ? (
  206. <PrefetchLink to={`/users/${ change.createdByUser.id }`}>
  207. {change.createdByUser.name
  208. || `名もなきニジラー(#${ change.createdByUser.id })`}
  209. </PrefetchLink>)
  210. : 'bot 操作'}
  211. <br/>
  212. {dateString (change.createdAt)}
  213. </td>
  214. <td className="p-2">
  215. <a
  216. href="#"
  217. onClick={async e => {
  218. e.preventDefault ()
  219. if (!(confirm (
  220. `『${ change.title.current
  221. || change.url.current }』を版 ${
  222. change.versionNo } に差戻します.\nよろしいですか?`)))
  223. return
  224. try
  225. {
  226. await apiPut (
  227. `/posts/${ change.postId }`,
  228. { title: change.title.current,
  229. tags: change.tags
  230. .filter (t => t.type !== 'removed')
  231. .map (t => t.name)
  232. .filter (t => t.slice (0, 5) !== 'nico:')
  233. .join (' '),
  234. parent_post_ids:
  235. (change.parentPosts ?? [])
  236. .filter (p => p.type !== 'removed')
  237. .map (p => p.id)
  238. .join (' '),
  239. original_created_from:
  240. change.originalCreatedFrom.current,
  241. original_created_before:
  242. change.originalCreatedBefore.current })
  243. qc.invalidateQueries ({ queryKey: postsKeys.root })
  244. qc.invalidateQueries ({ queryKey: tagsKeys.root })
  245. toast ({ description: '差戻しました.' })
  246. }
  247. catch
  248. {
  249. toast ({ description: '差戻に失敗……' })
  250. }
  251. }}>
  252. 復元
  253. </a>
  254. </td>
  255. </tr>)
  256. })}
  257. </tbody>
  258. </table>
  259. </div>
  260. <Pagination page={page} totalPages={totalPages}/>
  261. </>)}
  262. </MainArea>)
  263. }) satisfies FC