このコミットが含まれているのは:
+22
-8
@@ -3,6 +3,7 @@ import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motio
|
|||||||
import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { createBrowserRouter,
|
import { createBrowserRouter,
|
||||||
|
matchRoutes,
|
||||||
Navigate,
|
Navigate,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
useLocation,
|
useLocation,
|
||||||
@@ -16,9 +17,10 @@ import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
|||||||
import PageHead from '@/components/common/PageHead'
|
import PageHead from '@/components/common/PageHead'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
import { apiPost, isApiError } from '@/lib/api'
|
import { apiPost, isApiError } from '@/lib/api'
|
||||||
import { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
|
import { prepareHelpPage } from '@/lib/help'
|
||||||
|
import { fetchLocales, isLocaleCode, legacyLocalePath,
|
||||||
|
switchLocalePath, unlocalisedPath } from '@/lib/locales'
|
||||||
import { localesKeys } from '@/lib/queryKeys'
|
import { localesKeys } from '@/lib/queryKeys'
|
||||||
import { requiredPageLocale } from '@/lib/routeLocales'
|
|
||||||
import { useLocale } from '@/lib/useLocale'
|
import { useLocale } from '@/lib/useLocale'
|
||||||
import { applyClientAnimationMode,
|
import { applyClientAnimationMode,
|
||||||
applyClientAppearance,
|
applyClientAppearance,
|
||||||
@@ -37,10 +39,11 @@ import HelpDetailPage from '@/pages/help/HelpDetailPage'
|
|||||||
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
||||||
|
|
||||||
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
||||||
import type { RouteObject } from 'react-router-dom'
|
import type { RouteMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import type { ClientAnimationMode } from '@/lib/settings'
|
import type { ClientAnimationMode } from '@/lib/settings'
|
||||||
import type { AvailableLocale } from '@/lib/locales'
|
import type { AvailableLocale } from '@/lib/locales'
|
||||||
|
import type { PageRoute } from '@/lib/routeLocales'
|
||||||
import type { User } from '@/types'
|
import type { User } from '@/types'
|
||||||
|
|
||||||
const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage'))
|
const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage'))
|
||||||
@@ -80,7 +83,7 @@ const WikiSearchPage = lazy (() => import ('@/pages/wiki/WikiSearchPage'))
|
|||||||
const GekanatorRoute = ({ user }: { user: User | null }) => {
|
const GekanatorRoute = ({ user }: { user: User | null }) => {
|
||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const pageLocale = requiredPageLocale (location.pathname)
|
const pageLocale = resolvePageRoute (location.pathname)?.route.handle?.locale
|
||||||
|
|
||||||
if (pageLocale != null && pageLocale !== locale)
|
if (pageLocale != null && pageLocale !== locale)
|
||||||
return <GekanatorUnavailablePage/>
|
return <GekanatorUnavailablePage/>
|
||||||
@@ -95,7 +98,7 @@ const GekanatorRoute = ({ user }: { user: User | null }) => {
|
|||||||
export const pageRoutes = (
|
export const pageRoutes = (
|
||||||
user: User | null,
|
user: User | null,
|
||||||
setUser: Dispatch<SetStateAction<User | null>>,
|
setUser: Dispatch<SetStateAction<User | null>>,
|
||||||
): RouteObject[] => [
|
): PageRoute[] => [
|
||||||
{ path: '', element: <LocaleRedirect to="/posts"/> },
|
{ path: '', element: <LocaleRedirect to="/posts"/> },
|
||||||
{ path: 'posts', element: <PostListPage/> },
|
{ path: 'posts', element: <PostListPage/> },
|
||||||
{ path: 'posts/new', element: <PostNewPage user={user}/> },
|
{ path: 'posts/new', element: <PostNewPage user={user}/> },
|
||||||
@@ -124,13 +127,24 @@ export const pageRoutes = (
|
|||||||
{ path: 'help', element: <HelpListPage/> },
|
{ path: 'help', element: <HelpListPage/> },
|
||||||
{ path: 'help/changes', element: <HelpChangesPage/> },
|
{ path: 'help/changes', element: <HelpChangesPage/> },
|
||||||
{ path: 'help/:identifier/diff', element: <HelpDiffPage/> },
|
{ path: 'help/:identifier/diff', element: <HelpDiffPage/> },
|
||||||
{ path: 'help/:identifier', element: <HelpDetailPage/> },
|
{ id: 'help.detail', path: 'help/:identifier', element: <HelpDetailPage/>,
|
||||||
|
handle: { ssr: prepareHelpPage } },
|
||||||
{ path: 'users/settings', element: <SettingPage user={user} setUser={setUser}/> },
|
{ path: 'users/settings', element: <SettingPage user={user} setUser={setUser}/> },
|
||||||
{ path: 'settings', element: <LocaleRedirect to="/users/settings"/> },
|
{ path: 'settings', element: <LocaleRedirect to="/users/settings"/> },
|
||||||
{ path: 'tos', element: <TOSPage/> },
|
{ path: 'tos', element: <TOSPage/> },
|
||||||
{ path: 'gekanator', element: <GekanatorRoute user={user}/> },
|
{ id: 'gekanator', path: 'gekanator', element: <GekanatorRoute user={user}/>,
|
||||||
|
handle: { locale: 'ja' } },
|
||||||
{ path: 'more', element: <MorePage/> },
|
{ path: 'more', element: <MorePage/> },
|
||||||
{ path: '*', element: <NotFound/> }]
|
{ path: '*', element: <NotFound/>, handle: { notFound: true } }]
|
||||||
|
|
||||||
|
|
||||||
|
export const resolvePageRoute = (
|
||||||
|
pathname: string,
|
||||||
|
): RouteMatch<string, PageRoute> | null => {
|
||||||
|
const match = matchRoutes<PageRoute> (
|
||||||
|
pageRoutes (null, () => { }), unlocalisedPath (pathname))?.at (-1)
|
||||||
|
return (match == null || match.route.handle?.notFound) ? null : match
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
||||||
|
|||||||
@@ -74,12 +74,15 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
|
|||||||
|
|
||||||
const dialogue = useDialogue ()
|
const dialogue = useDialogue ()
|
||||||
|
|
||||||
const update = async (...args: Parameters<typeof updatePost>) => {
|
const update = async (
|
||||||
|
postData: Parameters<typeof updatePost>[0],
|
||||||
|
options: Parameters<typeof updatePost>[1],
|
||||||
|
) => {
|
||||||
clearValidationErrors ()
|
clearValidationErrors ()
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const data = await updatePost (...args, undefined, locale)
|
const data = await updatePost (postData, options, locale)
|
||||||
onSave ({ ...post,
|
onSave ({ ...post,
|
||||||
versionNo: data.versionNo,
|
versionNo: data.versionNo,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
const TOAST_LIMIT = 1
|
const TOAST_LIMIT = 1
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
type ToasterToast = Omit<ToastProps, 'title'> & {
|
||||||
id: string
|
id: string
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
description?: React.ReactNode
|
description?: React.ReactNode
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ import { QueryClient, QueryClientProvider, dehydrate } from '@tanstack/react-que
|
|||||||
import { renderToString } from 'react-dom/server'
|
import { renderToString } from 'react-dom/server'
|
||||||
import { HelmetProvider } from 'react-helmet-async'
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
import { I18nextProvider } from 'react-i18next'
|
import { I18nextProvider } from 'react-i18next'
|
||||||
import { matchRoutes } from 'react-router-dom'
|
|
||||||
import { createStaticHandler, createStaticRouter,
|
import { createStaticHandler, createStaticRouter,
|
||||||
StaticRouterProvider } from 'react-router-dom/server'
|
StaticRouterProvider } from 'react-router-dom/server'
|
||||||
|
|
||||||
import { AppFrame, appRoutes, pageRoutes } from '@/App'
|
import { AppFrame, appRoutes, resolvePageRoute } from '@/App'
|
||||||
import { matchHelpDetail, prepareHelpPage } from '@/lib/help'
|
|
||||||
import { createUII18n } from '@/lib/i18n'
|
import { createUII18n } from '@/lib/i18n'
|
||||||
import { fetchLocales, isLocaleCode, legacyLocalePath,
|
import { fetchLocales, isLocaleCode, legacyLocalePath } from '@/lib/locales'
|
||||||
unlocalisedPath } from '@/lib/locales'
|
|
||||||
import { localesKeys } from '@/lib/queryKeys'
|
import { localesKeys } from '@/lib/queryKeys'
|
||||||
import { withRenderContext } from '@/lib/renderContext.server'
|
import { withRenderContext } from '@/lib/renderContext.server'
|
||||||
|
|
||||||
@@ -35,13 +32,11 @@ export const render = async (
|
|||||||
const legacy = legacyLocalePath ({ pathname: url.pathname, search: url.search, hash: '' })
|
const legacy = legacyLocalePath ({ pathname: url.pathname, search: url.search, hash: '' })
|
||||||
const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname
|
const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname
|
||||||
const segment = pathname.split ('/')[1]
|
const segment = pathname.split ('/')[1]
|
||||||
const known = matchRoutes (pageRoutes (null, () => { })
|
const known = resolvePageRoute (pathname)
|
||||||
.filter (route => route.path !== '*'), unlocalisedPath (pathname))
|
|
||||||
if (!(isLocaleCode (segment)) || known == null)
|
if (!(isLocaleCode (segment)) || known == null)
|
||||||
return { status: 404 }
|
return { status: 404 }
|
||||||
if (legacy != null)
|
if (legacy != null)
|
||||||
return { status: 308, location: legacy }
|
return { status: 308, location: legacy }
|
||||||
const match = matchHelpDetail (url.pathname)
|
|
||||||
const locale = segment
|
const locale = segment
|
||||||
return withRenderContext ({ pathname: url.pathname, origin, apiBaseURL }, async () => {
|
return withRenderContext ({ pathname: url.pathname, origin, apiBaseURL }, async () => {
|
||||||
const client = new QueryClient ({ defaultOptions: {
|
const client = new QueryClient ({ defaultOptions: {
|
||||||
@@ -53,9 +48,11 @@ export const render = async (
|
|||||||
const active = locales.locales.some (entry => entry.code === locale)
|
const active = locales.locales.some (entry => entry.code === locale)
|
||||||
if (!(active))
|
if (!(active))
|
||||||
return { status: 404 as const }
|
return { status: 404 as const }
|
||||||
if (match == null || known.at (-1)?.route.path !== 'help/:identifier')
|
const prepare = known.route.handle?.ssr
|
||||||
|
if (prepare == null)
|
||||||
return null
|
return null
|
||||||
const status = await prepareHelpPage (client, url)
|
const status = await prepare ({ client, params: known.params, locale,
|
||||||
|
searchParams: url.searchParams })
|
||||||
const instance = createUII18n (locale)
|
const instance = createUII18n (locale)
|
||||||
const handler = createStaticHandler (appRoutes)
|
const handler = createStaticHandler (appRoutes)
|
||||||
const context = await handler.query (new Request (url))
|
const context = await handler.query (new Request (url))
|
||||||
|
|||||||
+10
-22
@@ -1,9 +1,10 @@
|
|||||||
import { isLocaleCode, localeLanguage } from '@/lib/locales'
|
import { localeLanguage } from '@/lib/locales'
|
||||||
import { helpKeys } from '@/lib/queryKeys'
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
|
|
||||||
import type { QueryClient } from '@tanstack/react-query'
|
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
|
import type { PagePreparation } from '@/lib/routeLocales'
|
||||||
|
|
||||||
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
|
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
|
||||||
|
|
||||||
export type ArticleMeta = {
|
export type ArticleMeta = {
|
||||||
@@ -97,34 +98,21 @@ export const resolveHelpLanguage = (
|
|||||||
: null
|
: null
|
||||||
|
|
||||||
|
|
||||||
export const matchHelpDetail = (pathname: string): RegExpMatchArray | null => {
|
|
||||||
const matched = pathname.match (/^\/([^/]+)\/help\/([^/]+)\/?$/)
|
|
||||||
return matched?.[2] === 'changes' ? null : matched
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MDX functions stay in memory; both entries load them before rendering.
|
// MDX functions stay in memory; both entries load them before rendering.
|
||||||
export const prepareHelpPage = async (client: QueryClient, url: URL): Promise<number> => {
|
export const prepareHelpPage = async (
|
||||||
const matched = matchHelpDetail (url.pathname)
|
{ client, params, locale, searchParams }: PagePreparation,
|
||||||
if (matched == null || !(isLocaleCode (matched[1])))
|
): Promise<number> => {
|
||||||
|
const identifier = params.identifier
|
||||||
|
if (identifier == null)
|
||||||
return 404
|
return 404
|
||||||
let identifier: string
|
|
||||||
try
|
|
||||||
{
|
|
||||||
identifier = decodeURIComponent (matched[2])
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return 404
|
|
||||||
}
|
|
||||||
const article = await client.fetchQuery ({
|
const article = await client.fetchQuery ({
|
||||||
queryKey: helpKeys.article (identifier),
|
queryKey: helpKeys.article (identifier),
|
||||||
queryFn: () => getHelpArticle (identifier), staleTime: Infinity })
|
queryFn: () => getHelpArticle (identifier), staleTime: Infinity })
|
||||||
const language = resolveHelpLanguage (article, localeLanguage (matched[1]))
|
const language = resolveHelpLanguage (article, localeLanguage (locale))
|
||||||
if (language == null)
|
if (language == null)
|
||||||
return 404
|
return 404
|
||||||
const meta = article!.languages[language]
|
const meta = article!.languages[language]
|
||||||
const edition = url.searchParams.get ('edition') ?? meta.currentEdition
|
const edition = searchParams.get ('edition') ?? meta.currentEdition
|
||||||
if (!(meta.publishedEditions.includes (edition)) || parseHelpEdition (edition) == null)
|
if (!(meta.publishedEditions.includes (edition)) || parseHelpEdition (edition) == null)
|
||||||
return 404
|
return 404
|
||||||
const content = await client.fetchQuery ({
|
const content = await client.fetchQuery ({
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ 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, url, locale) => {
|
const prefetchHelpIndex: Prefetcher = async (qc, _url, locale) => {
|
||||||
await qc.prefetchQuery ({
|
await qc.prefetchQuery ({
|
||||||
queryKey: helpKeys.index (localeLanguage (locale)),
|
queryKey: helpKeys.index (localeLanguage (locale)),
|
||||||
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
|
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
|
||||||
@@ -63,7 +63,7 @@ const prefetchHelpShow: Prefetcher = async (qc, url, locale) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const prefetchHelpChanges: Prefetcher = async (qc, url, locale) => {
|
const prefetchHelpChanges: Prefetcher = async (qc, _url, locale) => {
|
||||||
await qc.prefetchQuery ({
|
await qc.prefetchQuery ({
|
||||||
queryKey: helpKeys.changes (localeLanguage (locale)),
|
queryKey: helpKeys.changes (localeLanguage (locale)),
|
||||||
queryFn: () => getHelpChanges (localeLanguage (locale)),
|
queryFn: () => getHelpChanges (localeLanguage (locale)),
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { matchPath } from 'react-router-dom'
|
import type { QueryClient } from '@tanstack/react-query'
|
||||||
|
import type { IndexRouteObject, NonIndexRouteObject, Params } from 'react-router-dom'
|
||||||
import { unlocalisedPath } from '@/lib/locales'
|
|
||||||
|
|
||||||
import type { LocaleCode } from '@/lib/locales'
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
|
|
||||||
const PAGE_LOCALES: { path: string; locale: LocaleCode }[] = [
|
export type PagePreparation = {
|
||||||
{ path: '/gekanator', locale: 'ja' }]
|
client: QueryClient
|
||||||
|
params: Readonly<Params>
|
||||||
|
locale: LocaleCode
|
||||||
|
searchParams: URLSearchParams }
|
||||||
|
|
||||||
|
export type PageRouteHandle = {
|
||||||
|
ssr?: (request: PagePreparation) => Promise<number>
|
||||||
|
locale?: LocaleCode
|
||||||
|
notFound?: boolean }
|
||||||
|
|
||||||
export const requiredPageLocale = (pathname: string): LocaleCode | null =>
|
export type PageRoute = { handle?: PageRouteHandle } & (
|
||||||
PAGE_LOCALES.find (page =>
|
Omit<IndexRouteObject, 'handle'>
|
||||||
matchPath ({ path: page.path, end: true }, unlocalisedPath (pathname)))?.locale ?? null
|
| (Omit<NonIndexRouteObject, 'handle' | 'children'> & { children?: PageRoute[] }))
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"certified": "公証済み"
|
"certified": "公証済み"
|
||||||
},
|
},
|
||||||
"languageNames": {
|
"languageNames": {
|
||||||
"ja": "日本語",
|
"ja": "シサㇺ イタㇰ",
|
||||||
"ain": "ヤウンモシルン アイヌ イタㇰ"
|
"ain": "ヤウンモシルン アイヌ イタㇰ"
|
||||||
},
|
},
|
||||||
"revert": "アホシピレ",
|
"revert": "アホシピレ",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"languageNames": {
|
"languageNames": {
|
||||||
"ja": "日本語",
|
"ja": "日本語",
|
||||||
"ain": "ヤウンモシルン アイヌ イタㇰ"
|
"ain": "北海道アイヌ語"
|
||||||
},
|
},
|
||||||
"revert": "差戻",
|
"revert": "差戻",
|
||||||
"restored": "差戻しました.",
|
"restored": "差戻しました.",
|
||||||
|
|||||||
+8
-3
@@ -4,9 +4,8 @@ import { HelmetProvider } from 'react-helmet-async'
|
|||||||
import { I18nextProvider } from 'react-i18next'
|
import { I18nextProvider } from 'react-i18next'
|
||||||
|
|
||||||
import '@/index.css'
|
import '@/index.css'
|
||||||
import App from '@/App'
|
import App, { resolvePageRoute } from '@/App'
|
||||||
import { createUII18n } from '@/lib/i18n'
|
import { createUII18n } from '@/lib/i18n'
|
||||||
import { prepareHelpPage } from '@/lib/help'
|
|
||||||
import { currentViewingLocale } from '@/lib/locales'
|
import { currentViewingLocale } from '@/lib/locales'
|
||||||
import { localesKeys } from '@/lib/queryKeys'
|
import { localesKeys } from '@/lib/queryKeys'
|
||||||
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
|
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
|
||||||
@@ -66,7 +65,13 @@ const start = () => {
|
|||||||
hydrate (client, data.state)
|
hydrate (client, data.state)
|
||||||
if (client.getQueryData (localesKeys.index) == null)
|
if (client.getQueryData (localesKeys.index) == null)
|
||||||
throw new Error ('Missing SSR locales')
|
throw new Error ('Missing SSR locales')
|
||||||
const status = await prepareHelpPage (client, new URL (window.location.href))
|
const url = new URL (window.location.href)
|
||||||
|
const match = resolvePageRoute (url.pathname)
|
||||||
|
const prepare = match?.route.handle?.ssr
|
||||||
|
if (match == null || prepare == null)
|
||||||
|
throw new Error ('Missing SSR route preparation')
|
||||||
|
const status = await prepare ({ client, params: match.params,
|
||||||
|
locale: currentViewingLocale (), searchParams: url.searchParams })
|
||||||
if (status !== data.status)
|
if (status !== data.status)
|
||||||
throw new Error ('Server and browser Help versions differ')
|
throw new Error ('Server and browser Help versions differ')
|
||||||
mount (true)
|
mount (true)
|
||||||
|
|||||||
@@ -72,11 +72,10 @@ const renderPrimaryName = (
|
|||||||
language: string,
|
language: string,
|
||||||
label: (key: string) => ReactNode,
|
label: (key: string) => ReactNode,
|
||||||
) => (
|
) => (
|
||||||
<>
|
<span lang={language} className="whitespace-nowrap">
|
||||||
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
|
{snapshot.name}
|
||||||
{snapshot.name != null && snapshot.autoGenerated === true
|
{snapshot.name != null && snapshot.autoGenerated && label ('nameChanges.uncertified')}
|
||||||
&& label ('nameChanges.uncertified')}
|
</span>)
|
||||||
</>)
|
|
||||||
|
|
||||||
|
|
||||||
const renderAliasDiff = (
|
const renderAliasDiff = (
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする