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

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