feat: 耕作履歴ページ作成(#112) (#177)

#112 現在ページの表示を太く

#112 完了

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #177
This commit was merged in pull request #177.
This commit is contained in:
2025-12-14 03:49:03 +09:00
parent 9a656a9e6e
commit f36837f0d8
7 changed files with 220 additions and 0 deletions
+2
View File
@@ -9,6 +9,7 @@ import { API_BASE_URL } from '@/config'
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
import NotFound from '@/pages/NotFound'
import PostDetailPage from '@/pages/posts/PostDetailPage'
import PostHistoryPage from '@/pages/posts/PostHistoryPage'
import PostListPage from '@/pages/posts/PostListPage'
import PostNewPage from '@/pages/posts/PostNewPage'
import ServiceUnavailable from '@/pages/ServiceUnavailable'
@@ -79,6 +80,7 @@ export default (() => {
<Route path="/posts" element={<PostListPage/>}/>
<Route path="/posts/new" element={<PostNewPage user={user}/>}/>
<Route path="/posts/:id" element={<PostDetailPage user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/wiki" element={<WikiSearchPage/>}/>
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
+1
View File
@@ -30,6 +30,7 @@ export default (({ user }: Props) => {
{ name: '広場', to: '/posts', subMenu: [
{ name: '一覧', to: '/posts' },
{ name: '投稿追加', to: '/posts/new' },
{ name: '耕作履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [
{ name: 'タグ一覧', to: '/tags', visible: false },
@@ -0,0 +1,79 @@
import { Link, useLocation } from 'react-router-dom'
import type { FC } from 'react'
type Props = { page: number
totalPages: number
siblingCount?: number }
const range = (start: number, end: number): number[] =>
[...Array (end - start + 1).keys ()].map (i => start + i)
const getPages = (
page: number,
total: number,
siblingCount: number,
): (number | '…')[] => {
if (total <= 1)
return [1]
const first = 1
const last = total
const left = Math.max (page - siblingCount, first)
const right = Math.min (page + siblingCount, last)
const pages: (number | '…')[] = []
pages.push (first)
if (left > first + 1)
pages.push ('…')
const midStart = Math.max (left, first + 1)
const midEnd = Math.min (right, last - 1)
pages.push (...range (midStart, midEnd))
if (right < last - 1)
pages.push ('…')
if (last !== first)
pages.push (last)
return pages.filter ((v, i, arr) => i === 0 || v !== arr[i - 1])
}
export default (({ page, totalPages, siblingCount = 4 }) => {
const location = useLocation ()
const buildTo = (p: number) => {
const qs = new URLSearchParams (location.search)
qs.set ('page', String (p))
return `${ location.pathname }?${ qs.toString () }`
}
const pages = getPages (page, totalPages, siblingCount)
return (
<nav className="mt-4 flex justify-center" aria-label="Pagination">
<div className="flex items-center gap-2">
{(page > 1)
? <Link to={buildTo (page - 1)} aria-label="前のページ">&lt;</Link>
: <span aria-hidden>&lt;</span>}
{pages.map ((p, idx) => (
(p === '…')
? <span key={`dots-${ idx }`}></span>
: ((p === page)
? <span key={p} className="font-bold" aria-current="page">{p}</span>
: <Link key={p} to={buildTo (p)}>{p}</Link>)))}
{(page < totalPages)
? <Link to={buildTo (page + 1)} aria-label="次のページ">&gt;</Link>
: <span aria-hidden>&gt;</span>}
</div>
</nav>)
}) satisfies FC<Props>
@@ -0,0 +1,90 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation } from 'react-router-dom'
import TagLink from '@/components/TagLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config'
import type { FC } from 'react'
import type { PostTagChange } from '@/types'
export default (() => {
const [changes, setChanges] = useState<PostTagChange[]> ([])
const [totalPages, setTotalPages] = useState<number> (0)
const location = useLocation ()
const query = new URLSearchParams (location.search)
const id = query.get ('id')
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)
useEffect (() => {
void (async () => {
const res = await axios.get (`${ API_BASE_URL }/posts/changes`,
{ params: { ...(id && { id }),
...(page && { page }),
...(limit && { limit }) } })
const data = toCamel (res.data as any, { deep: true }) as {
changes: PostTagChange[]
count: number }
setChanges (data.changes)
setTotalPages (Math.trunc ((data.count - 1) / limit))
}) ()
}, [location.search])
return (
<MainArea>
<Helmet>
<title>{`耕作履歴 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageTitle>
{Boolean (id) && <>: 稿 {<Link to={`/posts/${ id }`}>#{id}</Link>}</>}
</PageTitle>
<table className="table-auto w-full border-collapse">
<thead>
<tr>
<th className="p-2 text-left">稿</th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
</tr>
</thead>
<tbody>
{changes.map (change => (
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
<td>
<Link to={`/posts/${ change.post.id }`}>
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
alt={change.post.title || change.post.url}
title={change.post.title || change.post.url || undefined}
className="w-40"/>
</Link>
</td>
<td>
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
{`${ change.changeType === 'add' ? '追加' : '削除' }`}
</td>
<td>
{change.user ? (
<Link to={`/users/${ change.user.id }`}>
{change.user.name}
</Link>) : 'bot 操作'}
<br/>
{change.timestamp}
</td>
</tr>))}
</tbody>
</table>
<Pagination page={page} totalPages={totalPages}/>
</MainArea>)
}) satisfies FC
+7
View File
@@ -29,6 +29,13 @@ export type Post = {
originalCreatedFrom: string | null
originalCreatedBefore: string | null }
export type PostTagChange = {
post: Post
tag: Tag
user?: User
changeType: 'add' | 'remove'
timestamp: string }
export type SubMenuItem =
| { component: ReactNode
visible: boolean }