| @@ -120,7 +120,7 @@ export default (({ user }: Props) => { | |||||
| const fetchPostCount = async () => { | const fetchPostCount = async () => { | ||||
| try | try | ||||
| { | { | ||||
| const wikiPage = await fetchWikiPage (String (wikiId ?? '')) | |||||
| const wikiPage = await fetchWikiPage (String (wikiId ?? ''), { }) | |||||
| const tag = await fetchTagByName (wikiPage.title) | const tag = await fetchTagByName (wikiPage.title) | ||||
| setPostCount (tag.postCount) | setPostCount (tag.postCount) | ||||
| @@ -1,6 +1,6 @@ | |||||
| import { apiDelete, apiGet, apiPost } from '@/lib/api' | import { apiDelete, apiGet, apiPost } from '@/lib/api' | ||||
| import type { Post } from '@/types' | |||||
| import type { Post, PostTagChange } from '@/types' | |||||
| export const fetchPosts = async ( | export const fetchPosts = async ( | ||||
| @@ -13,8 +13,8 @@ export const fetchPosts = async ( | |||||
| ): Promise<{ | ): Promise<{ | ||||
| posts: Post[] | posts: Post[] | ||||
| count: number | count: number | ||||
| nextCursor: string }> => await apiGet ('/posts', { | |||||
| params: { | |||||
| nextCursor: string }> => | |||||
| await apiGet ('/posts', { params: { | |||||
| tags, | tags, | ||||
| match, | match, | ||||
| ...(page && { page }), | ...(page && { page }), | ||||
| @@ -25,6 +25,17 @@ export const fetchPosts = async ( | |||||
| export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`) | 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> => { | export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => { | ||||
| await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`) | await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`) | ||||
| } | } | ||||
| @@ -1,14 +1,23 @@ | |||||
| import { QueryClient } from '@tanstack/react-query' | import { QueryClient } from '@tanstack/react-query' | ||||
| import { match } from 'path-to-regexp' | 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> | type Prefetcher = (qc: QueryClient, url: URL) => Promise<void> | ||||
| const mPost = match<{ id: string }> ('/posts/:id') | 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 prefetchPostsIndex: Prefetcher = async (qc, url) => { | ||||
| const tags = url.searchParams.get ('tags') ?? '' | const tags = url.searchParams.get ('tags') ?? '' | ||||
| const m = url.searchParams.get ('match') === 'any' ? 'any' : 'all' | 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 => 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> => { | 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 }) => | index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) => | ||||
| ['posts', 'index', p] as const, | ['posts', 'index', p] as const, | ||||
| show: (id: string) => ['posts', id] 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 = { | 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' | 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 ( | export const fetchWikiPageByTitle = async ( | ||||
| title: string, | title: string, | ||||
| { version }: { version?: string }, | { version }: { version?: string }, | ||||
| ): Promise<WikiPage> => | ): 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 { Helmet } from 'react-helmet-async' | ||||
| import { useLocation } from 'react-router-dom' | import { useLocation } from 'react-router-dom' | ||||
| @@ -9,17 +7,14 @@ import PrefetchLink from '@/components/PrefetchLink' | |||||
| import PageTitle from '@/components/common/PageTitle' | import PageTitle from '@/components/common/PageTitle' | ||||
| import Pagination from '@/components/common/Pagination' | import Pagination from '@/components/common/Pagination' | ||||
| import MainArea from '@/components/layout/MainArea' | 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 { FC } from 'react' | ||||
| import type { PostTagChange } from '@/types' | |||||
| export default (() => { | export default (() => { | ||||
| const [changes, setChanges] = useState<PostTagChange[]> ([]) | |||||
| const [totalPages, setTotalPages] = useState<number> (0) | |||||
| const location = useLocation () | const location = useLocation () | ||||
| const query = new URLSearchParams (location.search) | const query = new URLSearchParams (location.search) | ||||
| const id = query.get ('id') | const id = query.get ('id') | ||||
| @@ -29,17 +24,11 @@ export default (() => { | |||||
| // 投稿列の結合で使用 | // 投稿列の結合で使用 | ||||
| let rowsCnt: number | 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 ( | return ( | ||||
| <MainArea> | <MainArea> | ||||
| @@ -52,54 +41,57 @@ export default (() => { | |||||
| {id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>} | {id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>} | ||||
| </PageTitle> | </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>) | </MainArea>) | ||||
| }) satisfies FC | }) satisfies FC | ||||
| @@ -41,7 +41,7 @@ export default () => { | |||||
| setWikiPage (undefined) | setWikiPage (undefined) | ||||
| try | try | ||||
| { | { | ||||
| const data = await fetchWikiPage (title) | |||||
| const data = await fetchWikiPage (title, { }) | |||||
| navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true }) | ||||
| } | } | ||||
| catch | catch | ||||