Files
btrc-hub/frontend/src/pages/posts/PostHistoryPage.tsx
T
みてるぞ 48f823a7c8 履歴画面変更(#308) (#315)
Merge branch 'main' into feature/308

#308

#308

#308

#308

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #315
2026-04-18 05:43:33 +09:00

255 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import TagLink from '@/components/TagLink'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import { fetchPostChanges } from '@/lib/posts'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { fetchTag } from '@/lib/tags'
import { cn, dateString, originalCreatedAtString } from '@/lib/utils'
import type { FC } from 'react'
const renderDiff = (diff: { current: string | null; prev: string | null }) => (
<>
{(diff.prev && diff.prev !== diff.current) && (
<>
<del className="text-red-600 dark:text-red-400">
{diff.prev}
</del>
{diff.current && <br/>}
</>)}
{diff.current}
</>)
export default (() => {
const location = useLocation ()
const query = new URLSearchParams (location.search)
const id = query.get ('id')
const tagId = query.get ('tag')
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)
// 投稿列の結合で使用
let rowsCnt: number
const { data: tag } =
tagId
? useQuery ({ queryKey: tagsKeys.show (tagId),
queryFn: () => fetchTag (tagId) })
: { data: null }
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.changes ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
page, limit }),
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
page, limit }) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
const qc = useQueryClient ()
useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
}, [location.search])
const layoutIds: string[] = []
return (
<MainArea>
<Helmet>
<title>{`耕作履歴 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageTitle>
{id && <>: 稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
{tag && <><TagLink tag={tag} withWiki={false} withCount={false}/></>}
</PageTitle>
{loading ? 'Loading...' : (
<>
<div className="overflow-x-auto">
<table className="w-full min-w-[1200px] table-fixed border-collapse">
<colgroup>
{/* 投稿 */}
<col className="w-64"/>
{/* 版 */}
<col className="w-40"/>
{/* タイトル */}
<col className="w-96"/>
{/* URL */}
<col className="w-96"/>
{/* タグ */}
<col className="w-[48rem]"/>
{/* オリジナルの投稿日時 */}
<col className="w-96"/>
{/* 更新日時 */}
<col className="w-64"/>
{/* (差戻ボタン) */}
<col className="w-20"/>
</colgroup>
<thead className="border-b-2 border-black dark:border-white">
<tr>
<th className="p-2 text-left">稿</th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left">URL</th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left">稿</th>
<th className="p-2 text-left"></th>
<th className="p-2"/>
</tr>
</thead>
<tbody>
{changes.map ((change, i) => {
const withPost = i === 0 || change.postId !== changes[i - 1].postId
if (withPost)
{
rowsCnt = 1
for (let j = i + 1;
(j < changes.length
&& change.postId === changes[j].postId);
++j)
++rowsCnt
}
let layoutId: string | undefined = `page-${ change.postId }`
if (layoutIds.includes (layoutId))
layoutId = undefined
else
layoutIds.push (layoutId)
return (
<tr key={`${ change.postId }.${ change.versionNo }`}
className={cn ('even:bg-gray-100 dark:even:bg-gray-700',
withPost && 'border-t')}>
{withPost && (
<td className="align-top p-2 bg-white dark:bg-[#242424] border-r"
rowSpan={rowsCnt}>
<PrefetchLink to={`/posts/${ change.postId }`}>
<motion.div
layoutId={layoutId}
transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
<img src={change.thumbnail.current
|| change.thumbnailBase.current
|| undefined}
alt={change.title.current || change.url.current}
title={change.title.current || change.url.current || undefined}
className="w-40"/>
</motion.div>
</PrefetchLink>
</td>)}
<td className="p-2">{change.postId}.{change.versionNo}</td>
<td className="p-2 break-all">{renderDiff (change.title)}</td>
<td className="p-2 break-all">{renderDiff (change.url)}</td>
<td className="p-2">
{change.tags.map ((tag, i) => (
tag.type === 'added'
? (
<ins
key={i}
className="mr-2 text-green-600 dark:text-green-400">
{tag.name}
</ins>)
: (
tag.type === 'removed'
? (
<del
key={i}
className="mr-2 text-red-600 dark:text-red-400">
{tag.name}
</del>)
: (
<span key={i} className="mr-2">
{tag.name}
</span>))))}
</td>
<td className="p-2">
{change.versionNo === 1
? originalCreatedAtString (change.originalCreatedFrom.current,
change.originalCreatedBefore.current)
: renderDiff ({
current: originalCreatedAtString (
change.originalCreatedFrom.current,
change.originalCreatedBefore.current),
prev: originalCreatedAtString (
change.originalCreatedFrom.prev,
change.originalCreatedBefore.prev) })}
</td>
<td className="p-2">
{change.createdByUser
? (
<PrefetchLink to={`/users/${ change.createdByUser.id }`}>
{change.createdByUser.name
|| `名もなきニジラー(#${ change.createdByUser.id }`}
</PrefetchLink>)
: 'bot 操作'}
<br/>
{dateString (change.createdAt)}
</td>
<td className="p-2">
<a
href="#"
onClick={async e => {
e.preventDefault ()
if (!(confirm (
`${ change.title.current
|| change.url.current }』を版 ${
change.versionNo } に差戻します.\nよろしいですか?`)))
return
try
{
await apiPut (
`/posts/${ change.postId }`,
{ title: change.title.current,
tags: change.tags
.filter (t => t.type !== 'removed')
.map (t => t.name)
.filter (t => t.slice (0, 5) !== 'nico:')
.join (' '),
original_created_from:
change.originalCreatedFrom.current,
original_created_before:
change.originalCreatedBefore.current })
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '差戻しました.' })
}
catch
{
toast ({ description: '差戻に失敗……' })
}
}}>
</a>
</td>
</tr>)
})}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages}/>
</>)}
</MainArea>)
}) satisfies FC