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