| @@ -120,7 +120,7 @@ export default (({ user }: Props) => { | |||
| const fetchPostCount = async () => { | |||
| try | |||
| { | |||
| const wikiPage = await fetchWikiPage (String (wikiId ?? '')) | |||
| const wikiPage = await fetchWikiPage (String (wikiId ?? ''), { }) | |||
| const tag = await fetchTagByName (wikiPage.title) | |||
| setPostCount (tag.postCount) | |||
| @@ -1,6 +1,6 @@ | |||
| import { apiDelete, apiGet, apiPost } from '@/lib/api' | |||
| import type { Post } from '@/types' | |||
| import type { Post, PostTagChange } from '@/types' | |||
| export const fetchPosts = async ( | |||
| @@ -13,8 +13,8 @@ export const fetchPosts = async ( | |||
| ): Promise<{ | |||
| posts: Post[] | |||
| count: number | |||
| nextCursor: string }> => await apiGet ('/posts', { | |||
| params: { | |||
| nextCursor: string }> => | |||
| await apiGet ('/posts', { params: { | |||
| tags, | |||
| match, | |||
| ...(page && { page }), | |||
| @@ -25,6 +25,17 @@ export const fetchPosts = async ( | |||
| export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`) | |||
| export const fetchPostChanges = async ( | |||
| { id, page, limit }: { | |||
| id?: string | |||
| page: number | |||
| limit: number }, | |||
| ): Promise<{ | |||
| changes: PostTagChange[] | |||
| count: number }> => | |||
| await apiGet ('/posts/changes', { params: { ...(id && { id }), page, limit } }) | |||
| export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => { | |||
| await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`) | |||
| } | |||
| @@ -1,14 +1,23 @@ | |||
| import { QueryClient } from '@tanstack/react-query' | |||
| import { match } from 'path-to-regexp' | |||
| import { fetchPost, fetchPosts } from '@/lib/posts' | |||
| import { postsKeys } from '@/lib/queryKeys' | |||
| import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts' | |||
| import { postsKeys, wikiKeys } from '@/lib/queryKeys' | |||
| import { fetchWikiPages } from '@/lib/wiki' | |||
| type Prefetcher = (qc: QueryClient, url: URL) => Promise<void> | |||
| const mPost = match<{ id: string }> ('/posts/:id') | |||
| const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => { | |||
| const title = url.searchParams.get ('title') ?? '' | |||
| await qc.prefetchQuery ({ | |||
| queryKey: wikiKeys.index ({ title }), | |||
| queryFn: () => fetchWikiPages ({ title }) }) | |||
| } | |||
| const prefetchPostsIndex: Prefetcher = async (qc, url) => { | |||
| const tags = url.searchParams.get ('tags') ?? '' | |||
| const m = url.searchParams.get ('match') === 'any' ? 'any' : 'all' | |||
| @@ -32,11 +41,23 @@ const prefetchPostShow: Prefetcher = async (qc, url) => { | |||
| } | |||
| export const routePrefetchers: { | |||
| test: (u: URL) => boolean | |||
| run: Prefetcher }[] = [ | |||
| const prefetchPostChanges: Prefetcher = async (qc, url) => { | |||
| const id = url.searchParams.get ('id') | |||
| const page = Number (url.searchParams.get ('page') || 1) | |||
| const limit = Number (url.searchParams.get ('limit') || 20) | |||
| await qc.prefetchQuery ({ | |||
| queryKey: postsKeys.changes ({ ...(id && { id }), page, limit }), | |||
| queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) }) | |||
| } | |||
| export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [ | |||
| { test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex }, | |||
| { test: u => Boolean (mPost (u.pathname)), run: prefetchPostShow }] | |||
| { test: u => (['/posts/new', '/posts/changes'].includes(u.pathname) | |||
| && Boolean (mPost (u.pathname))), | |||
| run: prefetchPostShow }, | |||
| { test: u => u.pathname === '/posts/changes', run: prefetchPostChanges }, | |||
| { test: u => u.pathname === '/wiki', run: prefetchWikiPagesIndex }] | |||
| export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => { | |||
| @@ -3,8 +3,11 @@ export const postsKeys = { | |||
| index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) => | |||
| ['posts', 'index', p] as const, | |||
| show: (id: string) => ['posts', id] as const, | |||
| related: (id: string) => ['related', id] as const } | |||
| related: (id: string) => ['related', id] as const, | |||
| changes: (p: { id?: string; page: number; limit: number }) => | |||
| ['posts', 'changes', p] as const } | |||
| export const wikiKeys = { | |||
| root: ['wiki'] as const, | |||
| show: (title: string, p: { version: string }) => ['wiki', title, p] as const } | |||
| root: ['wiki'] as const, | |||
| index: (p: { title: string }) => ['wiki', 'index', p] as const, | |||
| show: (title: string, p: { version: string }) => ['wiki', title, p] as const } | |||
| @@ -3,12 +3,20 @@ import { apiGet } from '@/lib/api' | |||
| import type { WikiPage } from '@/types' | |||
| export const fetchWikiPage = async (id: string): Promise<WikiPage> => | |||
| await apiGet (`/wiki/${ id }`) | |||
| export const fetchWikiPages = async ({ title }: { title: string }) => | |||
| await apiGet ('/wiki', { params: { title } }) | |||
| export const fetchWikiPage = async ( | |||
| id: string, | |||
| { version }: { version?: string }, | |||
| ): Promise<WikiPage> => | |||
| await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } }) | |||
| export const fetchWikiPageByTitle = async ( | |||
| title: string, | |||
| { version }: { version?: string }, | |||
| ): Promise<WikiPage> => | |||
| await apiGet (`/wiki/title/${ title }`, { params: version ? { version } : { } }) | |||
| await apiGet (`/wiki/title/${ title }`, | |||
| { params: version ? { version } : { } }) | |||
| @@ -1,6 +1,4 @@ | |||
| import axios from 'axios' | |||
| import toCamel from 'camelcase-keys' | |||
| import { useEffect, useState } from 'react' | |||
| import { useQuery } from '@tanstack/react-query' | |||
| import { Helmet } from 'react-helmet-async' | |||
| import { useLocation } from 'react-router-dom' | |||
| @@ -9,17 +7,14 @@ import PrefetchLink from '@/components/PrefetchLink' | |||
| 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 { SITE_TITLE } from '@/config' | |||
| import { fetchPostChanges } from '@/lib/posts' | |||
| import { postsKeys } from '@/lib/queryKeys' | |||
| 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') | |||
| @@ -29,17 +24,11 @@ export default (() => { | |||
| // 投稿列の結合で使用 | |||
| let rowsCnt: number | |||
| useEffect (() => { | |||
| void (async () => { | |||
| const res = await axios.get (`${ API_BASE_URL }/posts/changes`, | |||
| { params: { ...(id && { id }), page, limit } }) | |||
| const data = toCamel (res.data as any, { deep: true }) as { | |||
| changes: PostTagChange[] | |||
| count: number } | |||
| setChanges (data.changes) | |||
| setTotalPages (Math.ceil (data.count / limit)) | |||
| }) () | |||
| }, [id, page, limit]) | |||
| const { data, isLoading: loading } = useQuery ({ | |||
| queryKey: postsKeys.changes ({ ...(id && { id }), page, limit }), | |||
| queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) }) | |||
| const changes = data?.changes ?? [] | |||
| const totalPages = data ? Math.ceil (data.count / limit) : 0 | |||
| return ( | |||
| <MainArea> | |||
| @@ -52,54 +41,57 @@ export default (() => { | |||
| {id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>} | |||
| </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, i) => { | |||
| let withPost = i === 0 || change.post.id !== changes[i - 1].post.id | |||
| if (withPost) | |||
| { | |||
| rowsCnt = 1 | |||
| for (let j = i + 1; | |||
| (j < changes.length | |||
| && change.post.id === changes[j].post.id); | |||
| ++j) | |||
| ++rowsCnt | |||
| } | |||
| return ( | |||
| <tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}> | |||
| {withPost && ( | |||
| <td className="align-top" rowSpan={rowsCnt}> | |||
| <PrefetchLink 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"/> | |||
| </PrefetchLink> | |||
| </td>)} | |||
| <td> | |||
| <TagLink tag={change.tag} withWiki={false} withCount={false}/> | |||
| {`を${ change.changeType === 'add' ? '追加' : '削除' }`} | |||
| </td> | |||
| <td> | |||
| {change.user ? ( | |||
| <PrefetchLink to={`/users/${ change.user.id }`}> | |||
| {change.user.name} | |||
| </PrefetchLink>) : 'bot 操作'} | |||
| <br/> | |||
| {change.timestamp} | |||
| </td> | |||
| </tr>) | |||
| })} | |||
| </tbody> | |||
| </table> | |||
| {loading ? 'Loading...' : ( | |||
| <> | |||
| <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, i) => { | |||
| let withPost = i === 0 || change.post.id !== changes[i - 1].post.id | |||
| if (withPost) | |||
| { | |||
| rowsCnt = 1 | |||
| for (let j = i + 1; | |||
| (j < changes.length | |||
| && change.post.id === changes[j].post.id); | |||
| ++j) | |||
| ++rowsCnt | |||
| } | |||
| return ( | |||
| <tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}> | |||
| {withPost && ( | |||
| <td className="align-top" rowSpan={rowsCnt}> | |||
| <PrefetchLink 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"/> | |||
| </PrefetchLink> | |||
| </td>)} | |||
| <td> | |||
| <TagLink tag={change.tag} withWiki={false} withCount={false}/> | |||
| {`を${ change.changeType === 'add' ? '記載' : '消除' }`} | |||
| </td> | |||
| <td> | |||
| {change.user ? ( | |||
| <PrefetchLink to={`/users/${ change.user.id }`}> | |||
| {change.user.name} | |||
| </PrefetchLink>) : 'bot 操作'} | |||
| <br/> | |||
| {change.timestamp} | |||
| </td> | |||
| </tr>) | |||
| })} | |||
| </tbody> | |||
| </table> | |||
| <Pagination page={page} totalPages={totalPages}/> | |||
| <Pagination page={page} totalPages={totalPages}/> | |||
| </>)} | |||
| </MainArea>) | |||
| }) satisfies FC | |||
| @@ -41,7 +41,7 @@ export default () => { | |||
| setWikiPage (undefined) | |||
| try | |||
| { | |||
| const data = await fetchWikiPage (title) | |||
| const data = await fetchWikiPage (title, { }) | |||
| navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | |||
| } | |||
| catch | |||