このコミットが含まれているのは:
@@ -38,6 +38,8 @@ import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
|||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
import TOSPage from '@/pages/TOSPage.mdx'
|
import TOSPage from '@/pages/TOSPage.mdx'
|
||||||
import HelpDetailPage from '@/pages/help/HelpDetailPage'
|
import HelpDetailPage from '@/pages/help/HelpDetailPage'
|
||||||
|
import HelpChangesPage from '@/pages/help/HelpChangesPage'
|
||||||
|
import HelpDiffPage from '@/pages/help/HelpDiffPage'
|
||||||
import HelpListPage from '@/pages/help/HelpListPage'
|
import HelpListPage from '@/pages/help/HelpListPage'
|
||||||
import PostDetailPage from '@/pages/posts/PostDetailPage'
|
import PostDetailPage from '@/pages/posts/PostDetailPage'
|
||||||
import PostHistoryPage from '@/pages/posts/PostHistoryPage'
|
import PostHistoryPage from '@/pages/posts/PostHistoryPage'
|
||||||
@@ -98,6 +100,8 @@ const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
|||||||
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
|
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
|
||||||
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
|
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
|
||||||
<Route path="/help" element={<HelpListPage/>}/>
|
<Route path="/help" element={<HelpListPage/>}/>
|
||||||
|
<Route path="/help/changes" element={<HelpChangesPage/>}/>
|
||||||
|
<Route path="/help/:identifier/diff" element={<HelpDiffPage/>}/>
|
||||||
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
|
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
|
||||||
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
|
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
|
||||||
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
|
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
|
||||||
|
|||||||
+138
-4
@@ -18,6 +18,18 @@ export type HelpSearchEntry = HelpEditionText & {
|
|||||||
identifier: string
|
identifier: string
|
||||||
edition: string }
|
edition: string }
|
||||||
|
|
||||||
|
export type HelpEditionContent = HelpEditionText & { content: ComponentType }
|
||||||
|
|
||||||
|
export type HelpEditionSource = HelpEditionText & { source: string }
|
||||||
|
export type HelpChange = HelpSearchEntry & { pred: string | null }
|
||||||
|
export type HelpDiffLine = {
|
||||||
|
type: 'added' | 'removed' | 'unchanged'
|
||||||
|
content: string }
|
||||||
|
export type HelpDiff = {
|
||||||
|
from: HelpEditionSource
|
||||||
|
to: HelpEditionSource
|
||||||
|
lines: HelpDiffLine[] }
|
||||||
|
|
||||||
const articleFiles = import.meta.glob<string> ('../../help/*/article.yml',
|
const articleFiles = import.meta.glob<string> ('../../help/*/article.yml',
|
||||||
{ query: '?raw',
|
{ query: '?raw',
|
||||||
import: 'default',
|
import: 'default',
|
||||||
@@ -72,6 +84,18 @@ export const getHelpEditions = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getHelpPublishedEditions = (
|
||||||
|
identifier: string,
|
||||||
|
language: string,
|
||||||
|
): string[] => {
|
||||||
|
const currentEdition = getHelpArticle (identifier)?.languages[language]?.currentEdition
|
||||||
|
return currentEdition == null
|
||||||
|
? []
|
||||||
|
: getHelpEditions (identifier, language)
|
||||||
|
.filter (edition => compareEditions (edition, currentEdition) <= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getHelpEdition = async (
|
export const getHelpEdition = async (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
language: string,
|
language: string,
|
||||||
@@ -87,28 +111,40 @@ export const getHelpEdition = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getHelpEditionText = async (
|
export const getHelpEditionSource = async (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
language: string,
|
language: string,
|
||||||
edition: string,
|
edition: string,
|
||||||
): Promise<HelpEditionText | null> => {
|
): Promise<HelpEditionSource | null> => {
|
||||||
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
|
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
|
||||||
const loader = rawEditionFiles[path]
|
const loader = rawEditionFiles[path]
|
||||||
|
|
||||||
if (!(loader))
|
if (!(loader))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const mdx = (await loader ()).replace (/^\uFEFF/, '')
|
const source = await loader ()
|
||||||
|
const mdx = source.replace (/^\uFEFF/, '')
|
||||||
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
|
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
|
||||||
|
|
||||||
if (!(heading))
|
if (!(heading))
|
||||||
throw new Error (`Help MDX の先頭に H1 がありません: ${ path }`)
|
throw new Error (`Help MDX の先頭に H1 がありません: ${ path }`)
|
||||||
|
|
||||||
return { title: heading[1].trim (),
|
return { source,
|
||||||
|
title: heading[1].trim (),
|
||||||
body: mdx.slice (heading[0].length) }
|
body: mdx.slice (heading[0].length) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getHelpEditionText = async (
|
||||||
|
identifier: string,
|
||||||
|
language: string,
|
||||||
|
edition: string,
|
||||||
|
): Promise<HelpEditionText | null> => {
|
||||||
|
const content = await getHelpEditionSource (identifier, language, edition)
|
||||||
|
return content == null ? null : { title: content.title, body: content.body }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getHelpSearchEntries = async (
|
export const getHelpSearchEntries = async (
|
||||||
language: string,
|
language: string,
|
||||||
): Promise<HelpSearchEntry[]> => {
|
): Promise<HelpSearchEntry[]> => {
|
||||||
@@ -130,3 +166,101 @@ export const getHelpSearchEntries = async (
|
|||||||
|
|
||||||
return entries.filter ((entry): entry is HelpSearchEntry => entry != null)
|
return entries.filter ((entry): entry is HelpSearchEntry => entry != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getHelpEditionContent = async (
|
||||||
|
identifier: string,
|
||||||
|
language: string,
|
||||||
|
edition: string,
|
||||||
|
): Promise<HelpEditionContent | null> => {
|
||||||
|
const [content, text] = await Promise.all ([
|
||||||
|
getHelpEdition (identifier, language, edition),
|
||||||
|
getHelpEditionText (identifier, language, edition)])
|
||||||
|
|
||||||
|
return content != null && text != null ? { content, ...text } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getHelpChanges = async (language: string): Promise<HelpChange[]> => {
|
||||||
|
const articles = Object.values (articleFiles).map (source =>
|
||||||
|
cameliseArticle (parseYAML (source)))
|
||||||
|
const changes = await Promise.all (articles.flatMap (article => {
|
||||||
|
const editions = getHelpPublishedEditions (article.identifier, language)
|
||||||
|
return editions.map (async (edition, index) => {
|
||||||
|
const text = await getHelpEditionText (article.identifier, language, edition)
|
||||||
|
if (text == null)
|
||||||
|
throw new Error (`Help edition が見つかりません: ${ article.identifier }/${ edition }`)
|
||||||
|
|
||||||
|
return { identifier: article.identifier,
|
||||||
|
edition, pred: editions[index - 1] ?? null, ...text }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
return changes.sort ((a, b) =>
|
||||||
|
compareEditions (b.edition, a.edition)
|
||||||
|
|| a.identifier.localeCompare (b.identifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const diffHelpLines = (from: string, to: string): HelpDiffLine[] => {
|
||||||
|
const before = from.match (/[^\n]*\n|[^\n]+$/g) ?? []
|
||||||
|
const after = to.match (/[^\n]*\n|[^\n]+$/g) ?? []
|
||||||
|
const lengths = Array.from ({ length: before.length + 1 },
|
||||||
|
() => new Uint32Array (after.length + 1))
|
||||||
|
|
||||||
|
for (let i = before.length - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
for (let j = after.length - 1; j >= 0; --j)
|
||||||
|
{
|
||||||
|
lengths[i][j] =
|
||||||
|
before[i] === after[j]
|
||||||
|
? lengths[i + 1][j + 1] + 1
|
||||||
|
: Math.max (lengths[i + 1][j], lengths[i][j + 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: HelpDiffLine[] = []
|
||||||
|
let i = 0
|
||||||
|
let j = 0
|
||||||
|
while (i < before.length || j < after.length)
|
||||||
|
{
|
||||||
|
if (i < before.length && j < after.length && before[i] === after[j])
|
||||||
|
{
|
||||||
|
lines.push ({ type: 'unchanged', content: before[i] })
|
||||||
|
++i
|
||||||
|
++j
|
||||||
|
}
|
||||||
|
else if (i < before.length
|
||||||
|
&& (j === after.length || lengths[i + 1][j] >= lengths[i][j + 1]))
|
||||||
|
{
|
||||||
|
lines.push ({ type: 'removed', content: before[i] })
|
||||||
|
++i
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lines.push ({ type: 'added', content: after[j] })
|
||||||
|
++j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getHelpDiff = async (
|
||||||
|
identifier: string,
|
||||||
|
language: string,
|
||||||
|
from: string,
|
||||||
|
to: string,
|
||||||
|
): Promise<HelpDiff | null> => {
|
||||||
|
const editions = getHelpPublishedEditions (identifier, language)
|
||||||
|
if (!(editions.includes (from)) || !(editions.includes (to)))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const [before, after] = await Promise.all ([
|
||||||
|
getHelpEditionSource (identifier, language, from),
|
||||||
|
getHelpEditionSource (identifier, language, to)])
|
||||||
|
if (before == null || after == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return { from: before, to: after, lines: diffHelpLines (before.source, after.source) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
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 { getHelpArticle,
|
||||||
|
getHelpChanges,
|
||||||
|
getHelpDiff,
|
||||||
|
getHelpEditionContent,
|
||||||
|
getHelpSearchEntries } from '@/lib/help'
|
||||||
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
||||||
import { fetchWikiPage,
|
import { fetchWikiPage,
|
||||||
fetchWikiPageByTitle,
|
fetchWikiPageByTitle,
|
||||||
@@ -15,12 +20,69 @@ type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
|||||||
const mPost = match<{ id: string }> ('/posts/:id')
|
const mPost = match<{ id: string }> ('/posts/:id')
|
||||||
const mWiki = match<{ title: string }> ('/wiki/:title')
|
const mWiki = match<{ title: string }> ('/wiki/:title')
|
||||||
const mTag = match<{ id: string }> ('/tags/:id')
|
const mTag = match<{ id: string }> ('/tags/:id')
|
||||||
|
const mHelp = match<{ identifier: string }> ('/help/:identifier')
|
||||||
|
const mHelpDiff = match<{ identifier: string }> ('/help/:identifier/diff')
|
||||||
|
|
||||||
|
|
||||||
const boolFromQuery = (value: string | null): boolean =>
|
const boolFromQuery = (value: string | null): boolean =>
|
||||||
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchHelpIndex: Prefetcher = async qc => {
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: helpKeys.index ('ja'),
|
||||||
|
queryFn: () => getHelpSearchEntries ('ja'),
|
||||||
|
staleTime: Infinity })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchHelpShow: Prefetcher = async (qc, url) => {
|
||||||
|
const matched = mHelp (url.pathname)
|
||||||
|
if (!(matched))
|
||||||
|
return
|
||||||
|
|
||||||
|
const { identifier } = matched.params
|
||||||
|
const language = 'ja'
|
||||||
|
const article = await qc.fetchQuery ({
|
||||||
|
queryKey: helpKeys.article (identifier),
|
||||||
|
queryFn: () => getHelpArticle (identifier),
|
||||||
|
staleTime: Infinity })
|
||||||
|
const edition = url.searchParams.get ('edition') ?? article?.languages[language]?.currentEdition
|
||||||
|
if (edition == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: helpKeys.edition (identifier, language, edition),
|
||||||
|
queryFn: () => getHelpEditionContent (identifier, language, edition),
|
||||||
|
staleTime: Infinity })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchHelpChanges: Prefetcher = async qc => {
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: helpKeys.changes ('ja'),
|
||||||
|
queryFn: () => getHelpChanges ('ja'),
|
||||||
|
staleTime: Infinity })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const prefetchHelpDiff: Prefetcher = async (qc, url) => {
|
||||||
|
const matched = mHelpDiff (url.pathname)
|
||||||
|
const fromValues = url.searchParams.getAll ('from')
|
||||||
|
const toValues = url.searchParams.getAll ('to')
|
||||||
|
const [from] = fromValues
|
||||||
|
const [to] = toValues
|
||||||
|
if (!(matched) || fromValues.length !== 1 || toValues.length !== 1 || !(from) || !(to))
|
||||||
|
return
|
||||||
|
|
||||||
|
const { identifier } = matched.params
|
||||||
|
await qc.prefetchQuery ({
|
||||||
|
queryKey: helpKeys.diff (identifier, 'ja', from, to),
|
||||||
|
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
|
||||||
|
staleTime: Infinity })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||||
const title = url.searchParams.get ('title') ?? ''
|
const title = url.searchParams.get ('title') ?? ''
|
||||||
|
|
||||||
@@ -223,6 +285,13 @@ const prefetchTagChanges: Prefetcher = async (qc, url) => {
|
|||||||
|
|
||||||
|
|
||||||
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
||||||
|
{ test: u => /^\/help\/?$/.test (u.pathname), run: prefetchHelpIndex },
|
||||||
|
{ test: u => /^\/help\/changes\/?$/.test (u.pathname), run: prefetchHelpChanges },
|
||||||
|
{ test: u => Boolean (mHelpDiff (u.pathname)), run: prefetchHelpDiff },
|
||||||
|
{ test: u => {
|
||||||
|
const matched = mHelp (u.pathname)
|
||||||
|
return Boolean (matched && matched.params.identifier !== 'changes')
|
||||||
|
}, run: prefetchHelpShow },
|
||||||
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
|
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
|
||||||
run: prefetchPostsIndex },
|
run: prefetchPostsIndex },
|
||||||
{ test: u => (!(['/posts/new', '/posts/changes', '/posts/search'].includes (u.pathname))
|
{ test: u => (!(['/posts/new', '/posts/changes', '/posts/search'].includes (u.pathname))
|
||||||
|
|||||||
@@ -61,3 +61,13 @@ export const wikiKeys = {
|
|||||||
root: ['wiki'] as const,
|
root: ['wiki'] as const,
|
||||||
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
|
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
|
||||||
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
|
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
|
||||||
|
|
||||||
|
export const helpKeys = {
|
||||||
|
root: ['help'] as const,
|
||||||
|
index: (language: string) => ['help', 'index', language] as const,
|
||||||
|
article: (identifier: string) => ['help', 'article', identifier] as const,
|
||||||
|
edition: (identifier: string, language: string, edition: string) =>
|
||||||
|
['help', 'edition', identifier, language, edition] as const,
|
||||||
|
diff: (identifier: string, language: string, from: string, to: string) =>
|
||||||
|
['help', 'diff', identifier, language, from, to] as const,
|
||||||
|
changes: (language: string) => ['help', 'changes', language] as const }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isAxiosError } from 'axios'
|
||||||
|
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { WikiPage } from '@/types'
|
import type { WikiPage } from '@/types'
|
||||||
@@ -21,8 +23,10 @@ export const fetchWikiPageByTitle = async (
|
|||||||
{
|
{
|
||||||
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
|
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
|
||||||
}
|
}
|
||||||
catch
|
catch (error)
|
||||||
{
|
{
|
||||||
|
if (isAxiosError (error) && error.response?.status === 404)
|
||||||
return null
|
return null
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,52 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
|
||||||
import type { MDXComponents } from 'mdx/types'
|
import type { MDXComponents } from 'mdx/types'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
|
||||||
|
|
||||||
export const useMDXComponents = (): MDXComponents => ({ })
|
const MDXLink = (
|
||||||
|
{ href, children, ...props }: ComponentProps<'a'>,
|
||||||
|
) => {
|
||||||
|
const location = useLocation ()
|
||||||
|
const url = useMemo (() => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const base = new URL (location.pathname + location.search, window.location.origin)
|
||||||
|
return href ? new URL (href, base) : null
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [href, location.pathname, location.search])
|
||||||
|
const internal = url != null
|
||||||
|
&& url.origin === window.location.origin
|
||||||
|
&& (url.protocol === 'http:' || url.protocol === 'https:')
|
||||||
|
const anchor = href?.startsWith ('#')
|
||||||
|
|| (url?.pathname === location.pathname
|
||||||
|
&& url.search === location.search && Boolean (url.hash))
|
||||||
|
const external = url != null && !(internal)
|
||||||
|
&& (url.protocol === 'http:' || url.protocol === 'https:')
|
||||||
|
|
||||||
|
if (internal && !(anchor) && props.download == null)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<PrefetchLink to={url.pathname + url.search + url.hash} {...props}>
|
||||||
|
{children}
|
||||||
|
</PrefetchLink>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={href}
|
||||||
|
{...props}
|
||||||
|
target={external ? props.target ?? '_blank' : props.target}
|
||||||
|
rel={props.rel ?? 'noopener noreferrer'}>
|
||||||
|
{children}
|
||||||
|
</a>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useMDXComponents = (): MDXComponents => ({ a: MDXLink })
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
|
import MainArea from '@/components/layout/MainArea'
|
||||||
|
import { SITE_TITLE } from '@/config'
|
||||||
|
import { getHelpChanges } from '@/lib/help'
|
||||||
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
|
import { dateString } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
const HelpChangesPage: FC = () => {
|
||||||
|
const { data: changes = [], isLoading, error } = useQuery ({
|
||||||
|
queryKey: helpKeys.changes ('ja'),
|
||||||
|
queryFn: () => getHelpChanges ('ja'),
|
||||||
|
staleTime: Infinity })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainArea>
|
||||||
|
<Helmet>
|
||||||
|
<title>{`ヘルプ 変更履歴 | ${ SITE_TITLE }`}</title>
|
||||||
|
</Helmet>
|
||||||
|
<PageTitle>変更履歴</PageTitle>
|
||||||
|
|
||||||
|
{isLoading
|
||||||
|
? <p className="mt-4">読み込み中...</p>
|
||||||
|
: (error
|
||||||
|
? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
|
||||||
|
: (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table-auto w-full whitespace-nowrap border-collapse">
|
||||||
|
<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">変更</th>
|
||||||
|
<th className="p-2 text-left">更新日時</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{changes.map (entry => {
|
||||||
|
const path = `/help/${ encodeURIComponent (entry.identifier) }`
|
||||||
|
const diffQuery = new URLSearchParams ({
|
||||||
|
from: entry.pred ?? '', to: entry.edition })
|
||||||
|
const updatedAt = `${ entry.edition.split ('.')[0] }T00:00:00`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={`${ entry.identifier }/${ entry.edition }`}
|
||||||
|
className="even:bg-gray-100 text-gray-900
|
||||||
|
dark:even:bg-gray-700 dark:text-gray-100">
|
||||||
|
<td className="p-2">
|
||||||
|
{entry.pred != null && (
|
||||||
|
<PrefetchLink to={`${ path }/diff?${ diffQuery.toString () }`}>
|
||||||
|
差分
|
||||||
|
</PrefetchLink>)}
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<PrefetchLink to={`${ path }?edition=${ entry.edition }`}>
|
||||||
|
{entry.title}
|
||||||
|
</PrefetchLink>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">{entry.edition}</td>
|
||||||
|
<td className="p-2">{entry.pred == null ? '新規' : '更新'}</td>
|
||||||
|
<td className="p-2">{dateString (updatedAt, 'hour')}</td>
|
||||||
|
</tr>)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>))}
|
||||||
|
</MainArea>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default HelpChangesPage
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useMemo } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { getHelpArticle, getHelpEdition, getHelpEditionText, getHelpEditions } from '@/lib/help'
|
import { getHelpArticle, getHelpEditionContent, getHelpEditions } from '@/lib/help'
|
||||||
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
|
|
||||||
import type { FC, ComponentType } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
|
||||||
const HelpDetailPage: FC = () => {
|
const HelpDetailPage: FC = () => {
|
||||||
@@ -18,9 +20,10 @@ const HelpDetailPage: FC = () => {
|
|||||||
// Locale 対応が入るまでは ja 固定
|
// Locale 対応が入るまでは ja 固定
|
||||||
const language = 'ja'
|
const language = 'ja'
|
||||||
|
|
||||||
const article = useMemo (
|
const { data: article, isLoading: articleLoading } = useQuery ({
|
||||||
() => getHelpArticle (identifier),
|
queryKey: helpKeys.article (identifier),
|
||||||
[identifier])
|
queryFn: () => getHelpArticle (identifier),
|
||||||
|
staleTime: Infinity })
|
||||||
|
|
||||||
const query = new URLSearchParams (location.search)
|
const query = new URLSearchParams (location.search)
|
||||||
const requestedEdition = query.get ('edition')
|
const requestedEdition = query.get ('edition')
|
||||||
@@ -39,51 +42,20 @@ const HelpDetailPage: FC = () => {
|
|||||||
? editions[index + 1]
|
? editions[index + 1]
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const [loaded, setLoaded] = useState<{ identifier: string
|
const { data: current, isSuccess, isError } = useQuery ({
|
||||||
language: string
|
enabled: edition != null && editions.includes (edition),
|
||||||
edition: string
|
queryKey: helpKeys.edition (identifier, language, edition ?? ''),
|
||||||
title: string
|
queryFn: () => getHelpEditionContent (identifier, language, edition ?? ''),
|
||||||
content: ComponentType | null } | null> (null)
|
staleTime: Infinity })
|
||||||
|
|
||||||
useEffect (() => {
|
if (!articleLoading
|
||||||
if (!(edition))
|
&& (article == null
|
||||||
return
|
|| languageMeta == null
|
||||||
|
|| edition == null
|
||||||
let active = true
|
|| !(editions.includes (edition))))
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
const component = await getHelpEdition (identifier, language, edition)
|
|
||||||
const text = await getHelpEditionText (identifier, language, edition)
|
|
||||||
|
|
||||||
if (active)
|
|
||||||
{
|
|
||||||
setLoaded ({ identifier,
|
|
||||||
language,
|
|
||||||
edition,
|
|
||||||
title: text?.title ?? '',
|
|
||||||
content: component })
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
active = false
|
|
||||||
}
|
|
||||||
}, [identifier, language, edition])
|
|
||||||
|
|
||||||
if (!(article)
|
|
||||||
|| !(languageMeta)
|
|
||||||
|| !(edition)
|
|
||||||
|| !(editions.includes (edition)))
|
|
||||||
return <NotFound/>
|
return <NotFound/>
|
||||||
|
|
||||||
const current =
|
if (isError || (isSuccess && current == null))
|
||||||
(loaded?.identifier === identifier
|
|
||||||
&& loaded?.language === language
|
|
||||||
&& loaded?.edition === edition)
|
|
||||||
? loaded
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (current && (!(current.content) || !(current.title)))
|
|
||||||
return <NotFound/>
|
return <NotFound/>
|
||||||
|
|
||||||
const Content = current?.content
|
const Content = current?.content
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
|
import MainArea from '@/components/layout/MainArea'
|
||||||
|
import { SITE_TITLE } from '@/config'
|
||||||
|
import { getHelpDiff } from '@/lib/help'
|
||||||
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import NotFound from '@/pages/NotFound'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
const markers = { added: '+ ', removed: '- ', unchanged: ' ' }
|
||||||
|
|
||||||
|
|
||||||
|
const HelpDiffPage: FC = () => {
|
||||||
|
const { identifier = '' } = useParams ()
|
||||||
|
const [searchParams] = useSearchParams ()
|
||||||
|
const from = searchParams.get ('from') ?? ''
|
||||||
|
const to = searchParams.get ('to') ?? ''
|
||||||
|
const validParams = Boolean (identifier && from && to)
|
||||||
|
&& searchParams.getAll ('from').length === 1
|
||||||
|
&& searchParams.getAll ('to').length === 1
|
||||||
|
const { data: diff, isLoading, isError } = useQuery ({
|
||||||
|
enabled: validParams,
|
||||||
|
queryKey: helpKeys.diff (identifier, 'ja', from, to),
|
||||||
|
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
|
||||||
|
staleTime: Infinity })
|
||||||
|
|
||||||
|
if (!(validParams) || isError || (!(isLoading) && diff == null))
|
||||||
|
return <NotFound/>
|
||||||
|
|
||||||
|
const title = `${ diff?.to.title ?? identifier } (${ from } → ${ to })`
|
||||||
|
const path = `/help/${ encodeURIComponent (identifier) }`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainArea>
|
||||||
|
<Helmet>
|
||||||
|
<title>{`ヘルプ 差分: ${ title } | ${ SITE_TITLE }`}</title>
|
||||||
|
</Helmet>
|
||||||
|
<PageTitle>{title}</PageTitle>
|
||||||
|
|
||||||
|
{diff
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<PrefetchLink to={`${ path }?edition=${ encodeURIComponent (from) }`}>
|
||||||
|
{diff.from.title} ({from})
|
||||||
|
</PrefetchLink>
|
||||||
|
<span>→</span>
|
||||||
|
<PrefetchLink to={`${ path }?edition=${ encodeURIComponent (to) }`}>
|
||||||
|
{diff.to.title} ({to})
|
||||||
|
</PrefetchLink>
|
||||||
|
</div>
|
||||||
|
<pre className="mt-4 overflow-x-auto whitespace-pre">
|
||||||
|
<code>
|
||||||
|
{diff.lines.map ((line, index) => {
|
||||||
|
const colour = cn (
|
||||||
|
'block text-gray-900 dark:text-gray-100',
|
||||||
|
line.type === 'added' && 'bg-green-200 dark:bg-green-800',
|
||||||
|
line.type === 'added' && 'text-green-950 dark:text-green-50',
|
||||||
|
line.type === 'removed' && 'bg-red-200 dark:bg-red-800',
|
||||||
|
line.type === 'removed' && 'text-red-950 dark:text-red-50')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={index} className={colour}>
|
||||||
|
{markers[line.type]}{line.content.replace (/\r?\n$/, '')}
|
||||||
|
</span>)
|
||||||
|
})}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</>)
|
||||||
|
: <div>Loading...</div>}
|
||||||
|
</MainArea>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default HelpDiffPage
|
||||||
@@ -1,56 +1,54 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
import SortHeader from '@/components/SortHeader'
|
||||||
import FormField from '@/components/common/FormField'
|
import FormField from '@/components/common/FormField'
|
||||||
import PageTitle from '@/components/common/PageTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { getHelpSearchEntries } from '@/lib/help'
|
import { getHelpSearchEntries } from '@/lib/help'
|
||||||
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
import { dateString, inputClass } from '@/lib/utils'
|
import { dateString, inputClass } from '@/lib/utils'
|
||||||
|
|
||||||
import type { FormEvent, FC } from 'react'
|
import type { FormEvent, FC } from 'react'
|
||||||
|
|
||||||
import type { HelpSearchEntry } from '@/lib/help'
|
|
||||||
|
|
||||||
type SearchConditions = { title: string; text: string }
|
type SearchConditions = { title: string; text: string }
|
||||||
|
type HelpOrderField = 'title' | 'updated_at'
|
||||||
|
type HelpOrder = `${ HelpOrderField }:${ 'asc' | 'desc' }`
|
||||||
|
|
||||||
|
const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
|
||||||
|
title: 'asc', updated_at: 'desc' }
|
||||||
|
|
||||||
|
|
||||||
const HelpSearchPage: FC = () => {
|
const HelpSearchPage: FC = () => {
|
||||||
const [title, setTitle] = useState ('')
|
const [searchParams, setSearchParams] = useSearchParams ()
|
||||||
const [text, setText] = useState ('')
|
const requestedOrder = searchParams.get ('order') ?? 'updated_at:desc'
|
||||||
const [conditions, setConditions] = useState<SearchConditions> ({ title: '', text: '' })
|
const order: HelpOrder =
|
||||||
const [entries, setEntries] = useState<HelpSearchEntry[]> ([])
|
['title:asc', 'title:desc', 'updated_at:asc', 'updated_at:desc'].includes (requestedOrder)
|
||||||
const [loading, setLoading] = useState (true)
|
? requestedOrder as HelpOrder
|
||||||
const [error, setError] = useState<string | null> (null)
|
: 'updated_at:desc'
|
||||||
|
const conditions = useMemo<SearchConditions> (() => ({
|
||||||
|
title: searchParams.get ('title') ?? '',
|
||||||
|
text: searchParams.get ('text') ?? '' }), [searchParams])
|
||||||
|
const [title, setTitle] = useState (conditions.title)
|
||||||
|
const [text, setText] = useState (conditions.text)
|
||||||
|
const { data: entries = [], isLoading: loading, error } = useQuery ({
|
||||||
|
queryKey: helpKeys.index ('ja'),
|
||||||
|
queryFn: () => getHelpSearchEntries ('ja'),
|
||||||
|
staleTime: Infinity })
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
let active = true
|
setTitle (conditions.title)
|
||||||
|
setText (conditions.text)
|
||||||
void (async () => {
|
}, [conditions.title, conditions.text])
|
||||||
try
|
|
||||||
{
|
|
||||||
if (active)
|
|
||||||
setEntries (await getHelpSearchEntries ('ja'))
|
|
||||||
}
|
|
||||||
catch (err)
|
|
||||||
{
|
|
||||||
if (active)
|
|
||||||
setError (err instanceof Error ? err.message : '取得に失敗しました.')
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (active)
|
|
||||||
setLoading (false)
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return () => { active = false }
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const results = useMemo (() => {
|
const results = useMemo (() => {
|
||||||
const titleQuery = conditions.title.trim ().normalize ('NFKC').toLowerCase ()
|
const titleQuery = conditions.title.trim ().normalize ('NFKC').toLowerCase ()
|
||||||
const textQuery = conditions.text.trim ().normalize ('NFKC').toLowerCase ()
|
const textQuery = conditions.text.trim ().normalize ('NFKC').toLowerCase ()
|
||||||
|
const [field, direction] = order.split (':')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
entries.filter (entry => (
|
entries.filter (entry => (
|
||||||
@@ -58,12 +56,30 @@ const HelpSearchPage: FC = () => {
|
|||||||
|| entry.title.normalize ('NFKC').toLowerCase ().includes (titleQuery))
|
|| entry.title.normalize ('NFKC').toLowerCase ().includes (titleQuery))
|
||||||
&& (!(textQuery)
|
&& (!(textQuery)
|
||||||
|| entry.body.normalize ('NFKC').toLowerCase ().includes (textQuery))))
|
|| entry.body.normalize ('NFKC').toLowerCase ().includes (textQuery))))
|
||||||
.sort ((a, b) => a.title.localeCompare (b.title, 'ja')))
|
.sort ((a, b) => {
|
||||||
}, [entries, conditions])
|
const [dateA, revisionA = '1'] = a.edition.split ('.')
|
||||||
|
const [dateB, revisionB = '1'] = b.edition.split ('.')
|
||||||
|
const comparison = field === 'title'
|
||||||
|
? a.title.localeCompare (b.title, 'ja')
|
||||||
|
: (dateA.localeCompare (dateB) || Number (revisionA) - Number (revisionB))
|
||||||
|
|
||||||
|
return (direction === 'asc' ? comparison : -comparison)
|
||||||
|
|| a.identifier.localeCompare (b.identifier)
|
||||||
|
}))
|
||||||
|
}, [entries, conditions, order])
|
||||||
|
|
||||||
const handleSearch = (ev: FormEvent<HTMLFormElement>) => {
|
const handleSearch = (ev: FormEvent<HTMLFormElement>) => {
|
||||||
ev.preventDefault ()
|
ev.preventDefault ()
|
||||||
setConditions ({ title, text })
|
const next = new URLSearchParams (searchParams)
|
||||||
|
if (title)
|
||||||
|
next.set ('title', title)
|
||||||
|
else
|
||||||
|
next.delete ('title')
|
||||||
|
if (text)
|
||||||
|
next.set ('text', text)
|
||||||
|
else
|
||||||
|
next.delete ('text')
|
||||||
|
setSearchParams (next)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,15 +121,27 @@ const HelpSearchPage: FC = () => {
|
|||||||
{loading
|
{loading
|
||||||
? <p className="mt-4">読み込み中...</p>
|
? <p className="mt-4">読み込み中...</p>
|
||||||
: (error
|
: (error
|
||||||
? <p className="mt-4 text-red-600" role="alert">{error}</p>
|
? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
|
||||||
: (
|
: (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p>
|
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p>
|
||||||
<table className="table-auto w-full border-collapse">
|
<table className="table-auto w-full border-collapse">
|
||||||
<thead className="border-b-2 border-black dark:border-white">
|
<thead className="border-b-2 border-black dark:border-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 text-left">タイトル</th>
|
<th className="p-2 text-left">
|
||||||
<th className="p-2 text-left">最終更新</th>
|
<SortHeader<HelpOrderField>
|
||||||
|
by="title"
|
||||||
|
label="タイトル"
|
||||||
|
currentOrder={order}
|
||||||
|
defaultDirection={defaultDirection}/>
|
||||||
|
</th>
|
||||||
|
<th className="p-2 text-left">
|
||||||
|
<SortHeader<HelpOrderField>
|
||||||
|
by="updated_at"
|
||||||
|
label="最終更新"
|
||||||
|
currentOrder={order}
|
||||||
|
defaultDirection={defaultDirection}/>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import type { FC } from 'react'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
@@ -13,11 +11,14 @@ import TabGroup, { Tab } from '@/components/common/TabGroup'
|
|||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
|
import { getHelpChanges } from '@/lib/help'
|
||||||
import { fetchPosts } from '@/lib/posts'
|
import { fetchPosts } from '@/lib/posts'
|
||||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { getHelpSearchEntries } from '@/lib/help'
|
|
||||||
import { fetchTagByName } from '@/lib/tags'
|
import { fetchTagByName } from '@/lib/tags'
|
||||||
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
||||||
|
import NotFound from '@/pages/NotFound'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
import type { Tag } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
@@ -28,28 +29,46 @@ const WikiDetailPage: FC = () => {
|
|||||||
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
|
const queryClient = useQueryClient ()
|
||||||
void (async () => {
|
|
||||||
const identifier =
|
|
||||||
(await getHelpSearchEntries ('ja'))
|
|
||||||
.find (entry => (entry.title.trim ().normalize ('NFKC').toLowerCase ()
|
|
||||||
=== (title.trim ().normalize ('NFKC').toLowerCase ()
|
|
||||||
.replace (/^ヘルプ:/, ''))))
|
|
||||||
.identifier
|
|
||||||
|
|
||||||
navigate (`/help/${ identifier }`, { replace: true })
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
|
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
|
||||||
|
|
||||||
const query = new URLSearchParams (location.search)
|
const query = new URLSearchParams (location.search)
|
||||||
const version = query.get ('version') || undefined
|
const version = query.get ('version') || undefined
|
||||||
|
|
||||||
const { data: wikiPage, isLoading: loading } = useQuery ({
|
const { data: wikiPage, isLoading: loading, isSuccess, isFetching } = useQuery ({
|
||||||
enabled: Boolean (title) && !(/^\d+$/.test (title)),
|
enabled: Boolean (title) && !(/^\d+$/.test (title)),
|
||||||
queryKey: wikiKeys.show (title, { version }),
|
queryKey: wikiKeys.show (title, { version }),
|
||||||
queryFn: () => fetchWikiPageByTitle (title, { version }) })
|
queryFn: () => fetchWikiPageByTitle (title, { version }) })
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
if (!(isSuccess) || isFetching || wikiPage != null)
|
||||||
|
return
|
||||||
|
|
||||||
|
let active = true
|
||||||
|
const lookupTitle = title.trim ().normalize ('NFKC').toLowerCase ().replace (/^ヘルプ:/, '')
|
||||||
|
|
||||||
|
void queryClient.fetchQuery ({
|
||||||
|
queryKey: helpKeys.changes ('ja'),
|
||||||
|
queryFn: () => getHelpChanges ('ja'),
|
||||||
|
staleTime: Infinity }).then (changes => {
|
||||||
|
if (!(active))
|
||||||
|
return
|
||||||
|
|
||||||
|
const identifiers = new Set (changes
|
||||||
|
.filter (entry => entry.title.trim ().normalize ('NFKC').toLowerCase () === lookupTitle)
|
||||||
|
.map (entry => entry.identifier))
|
||||||
|
if (identifiers.size === 1)
|
||||||
|
navigate (`/help/${ encodeURIComponent ([...identifiers][0]) }`, { replace: true })
|
||||||
|
}).catch (() => {
|
||||||
|
// A failed Help lookup leaves the original missing Wiki URL in place.
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
}, [isSuccess, isFetching, wikiPage, title, version, queryClient, navigate])
|
||||||
|
|
||||||
const effectiveTitle = wikiPage?.title ?? title
|
const effectiveTitle = wikiPage?.title ?? title
|
||||||
const deprecated = wikiPage?.deprecatedAt != null
|
const deprecated = wikiPage?.deprecatedAt != null
|
||||||
|
|
||||||
@@ -85,19 +104,28 @@ const WikiDetailPage: FC = () => {
|
|||||||
if (!(/^\d+$/.test (title)))
|
if (!(/^\d+$/.test (title)))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
let active = true
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const data = await fetchWikiPage (title, { })
|
const data = await fetchWikiPage (title, { })
|
||||||
navigate (`/wiki/${ encodeURIComponent(data.title) }`, { replace: true })
|
if (active)
|
||||||
|
navigate (`/wiki/${ encodeURIComponent (data.title) }`, { replace: true })
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}) ()
|
}) ()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
}, [title, navigate])
|
}, [title, navigate])
|
||||||
|
|
||||||
|
if (isSuccess && wikiPage == null)
|
||||||
|
return <NotFound/>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする