Reviewed-on: #433 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
+290
-125
@@ -1,18 +1,31 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { createElement, Suspense, lazy, useEffect,
|
||||
useLayoutEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createBrowserRouter,
|
||||
matchRoutes,
|
||||
Navigate,
|
||||
Route,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
useLocation } from 'react-router-dom'
|
||||
useLocation,
|
||||
useParams,
|
||||
useRoutes } from 'react-router-dom'
|
||||
|
||||
import DevModeWatermark from '@/components/DevModeWatermark'
|
||||
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
||||
import TopNav from '@/components/TopNav'
|
||||
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 { isApiError } from '@/lib/api'
|
||||
import { initialiseUserSession } from '@/lib/users'
|
||||
import { prepareHelpPage } from '@/lib/help'
|
||||
import { fetchLocales, isLocaleCode, legacyLocalePath,
|
||||
switchLocalePath, unlocalisedPath } from '@/lib/locales'
|
||||
import { localesKeys, sessionKeys } from '@/lib/queryKeys'
|
||||
import { switchPostFilterLocale } from '@/lib/posts'
|
||||
import { switchWikiLocale } from '@/lib/wiki'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { applyClientAnimationMode,
|
||||
applyClientAppearance,
|
||||
fetchUserThemeSlots,
|
||||
@@ -25,98 +38,196 @@ import { applyClientAnimationMode,
|
||||
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
||||
import { UnsavedChangesGuardProvider } from '@/lib/useUnsavedChangesGuard'
|
||||
import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts'
|
||||
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
|
||||
import MaterialBasePage from '@/pages/materials/MaterialBasePage'
|
||||
import MaterialDetailPage from '@/pages/materials/MaterialDetailPage'
|
||||
import MaterialHistoryPage from '@/pages/materials/MaterialHistoryPage'
|
||||
import MaterialListPage from '@/pages/materials/MaterialListPage'
|
||||
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
||||
import MaterialSyncSuppressionsPage from '@/pages/materials/MaterialSyncSuppressionsPage'
|
||||
import MorePage from '@/pages/MorePage'
|
||||
import GekanatorPage from '@/pages/GekanatorPage'
|
||||
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
import TOSPage from '@/pages/TOSPage.mdx'
|
||||
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 PostDetailPage from '@/pages/posts/PostDetailPage'
|
||||
import PostHistoryPage from '@/pages/posts/PostHistoryPage'
|
||||
import PostNewPage from '@/pages/posts/PostNewPage'
|
||||
import PostListPage from '@/pages/posts/PostListPage'
|
||||
import PostSearchPage from '@/pages/posts/PostSearchPage'
|
||||
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
||||
import SettingPage from '@/pages/users/SettingPage'
|
||||
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
||||
import TagHistoryPage from '@/pages/tags/TagHistoryPage'
|
||||
import TagListPage from '@/pages/tags/TagListPage'
|
||||
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
|
||||
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
|
||||
import WikiDiffPage from '@/pages/wiki/WikiDiffPage'
|
||||
import WikiEditPage from '@/pages/wiki/WikiEditPage'
|
||||
import WikiHistoryPage from '@/pages/wiki/WikiHistoryPage'
|
||||
import WikiNewPage from '@/pages/wiki/WikiNewPage'
|
||||
import WikiSearchPage from '@/pages/wiki/WikiSearchPage'
|
||||
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
import type { ComponentType, Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
||||
import type { RouteMatch } from 'react-router-dom'
|
||||
|
||||
import type { ClientAnimationMode } from '@/lib/settings'
|
||||
import type { AvailableLocale, LocaleCode } from '@/lib/locales'
|
||||
import type { PageRoute } from '@/lib/routeLocales'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const lazyPage = <Props extends object,> (
|
||||
load: () => Promise<{ default: ComponentType<Props> }>,
|
||||
) => {
|
||||
let loaded: ComponentType<Props> | null = null
|
||||
let pending: ReturnType<typeof load> | null = null
|
||||
const preload = () => pending ??= load ().then (module => {
|
||||
loaded = module.default
|
||||
return module
|
||||
}).catch (error => {
|
||||
pending = null
|
||||
throw error
|
||||
})
|
||||
const Deferred = lazy (preload)
|
||||
// A preloaded page must render synchronously on the first hydration pass.
|
||||
const Page = (props: Props) => createElement (loaded ?? Deferred, props)
|
||||
return Object.assign (Page, { preload })
|
||||
}
|
||||
|
||||
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
||||
animationMode: ClientAnimationMode
|
||||
user: User | null
|
||||
setUser: Dispatch<SetStateAction<User | null>> }) => {
|
||||
|
||||
const GekanatorPage = lazyPage (() => import ('@/pages/GekanatorPage'))
|
||||
const DeerjikistDetailPage = lazyPage (() => import ('@/pages/deerjikists/DeerjikistDetailPage'))
|
||||
const MaterialBasePage = lazyPage (() => import ('@/pages/materials/MaterialBasePage'))
|
||||
const MaterialDetailPage = lazyPage (() => import ('@/pages/materials/MaterialDetailPage'))
|
||||
const MaterialHistoryPage = lazyPage (() => import ('@/pages/materials/MaterialHistoryPage'))
|
||||
const MaterialListPage = lazyPage (() => import ('@/pages/materials/MaterialListPage'))
|
||||
const MaterialNewPage = lazyPage (() => import ('@/pages/materials/MaterialNewPage'))
|
||||
const MaterialSyncSuppressionsPage = lazyPage (
|
||||
() => import ('@/pages/materials/MaterialSyncSuppressionsPage'))
|
||||
const MorePage = lazyPage (() => import ('@/pages/MorePage'))
|
||||
const GekanatorUnavailablePage = lazyPage (() => import ('@/pages/GekanatorUnavailablePage'))
|
||||
const NicoTagListPage = lazyPage (() => import ('@/pages/tags/NicoTagListPage'))
|
||||
const TOSPage = lazyPage (() => import ('@/pages/TOSPage'))
|
||||
const HelpChangesPage = lazyPage (() => import ('@/pages/help/HelpChangesPage'))
|
||||
const HelpDiffPage = lazyPage (() => import ('@/pages/help/HelpDiffPage'))
|
||||
const HelpListPage = lazyPage (() => import ('@/pages/help/HelpListPage'))
|
||||
const PostDetailPage = lazyPage (() => import ('@/pages/posts/PostDetailPage'))
|
||||
const PostHistoryPage = lazyPage (() => import ('@/pages/posts/PostHistoryPage'))
|
||||
const PostNewPage = lazyPage (() => import ('@/pages/posts/PostNewPage'))
|
||||
const PostListPage = lazyPage (() => import ('@/pages/posts/PostListPage'))
|
||||
const PostSearchPage = lazyPage (() => import ('@/pages/posts/PostSearchPage'))
|
||||
const SettingPage = lazyPage (() => import ('@/pages/users/SettingPage'))
|
||||
const TagDetailPage = lazyPage (() => import ('@/pages/tags/TagDetailPage'))
|
||||
const TagHistoryPage = lazyPage (() => import ('@/pages/tags/TagHistoryPage'))
|
||||
const TagListPage = lazyPage (() => import ('@/pages/tags/TagListPage'))
|
||||
const TheatreDetailPage = lazyPage (() => import ('@/pages/theatres/TheatreDetailPage'))
|
||||
const WikiDetailPage = lazyPage (() => import ('@/pages/wiki/WikiDetailPage'))
|
||||
const WikiDiffPage = lazyPage (() => import ('@/pages/wiki/WikiDiffPage'))
|
||||
const WikiEditPage = lazyPage (() => import ('@/pages/wiki/WikiEditPage'))
|
||||
const WikiHistoryPage = lazyPage (() => import ('@/pages/wiki/WikiHistoryPage'))
|
||||
const WikiNewPage = lazyPage (() => import ('@/pages/wiki/WikiNewPage'))
|
||||
const WikiSearchPage = lazyPage (() => import ('@/pages/wiki/WikiSearchPage'))
|
||||
|
||||
|
||||
const GekanatorRoute = ({ user }: { user: User | null }) => {
|
||||
const locale = useLocale ()
|
||||
const location = useLocation ()
|
||||
const pageLocale = resolvePageRoute (location.pathname)?.route.handle?.locale
|
||||
|
||||
const routes = (
|
||||
<Routes location={location}>
|
||||
<Route path="/" element={<Navigate to="/posts" replace/>}/>
|
||||
<Route path="/posts" element={<PostListPage/>}/>
|
||||
<Route path="/posts/new" element={<PostNewPage user={user}/>}/>
|
||||
<Route path="/posts/search" element={<PostSearchPage/>}/>
|
||||
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
|
||||
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
|
||||
<Route path="/tags" element={<TagListPage/>}/>
|
||||
<Route path="/tags/:id" element={<TagDetailPage/>}/>
|
||||
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
|
||||
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
|
||||
<Route path="/nico/tags" element={<NicoTagListPage user={user}/>}/>
|
||||
<Route path="/tags/changes" element={<TagHistoryPage/>}/>
|
||||
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
|
||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||
<Route index element={<MaterialListPage/>}/>
|
||||
<Route path="changes" element={<MaterialHistoryPage/>}/>
|
||||
<Route path="new" element={<MaterialNewPage/>}/>
|
||||
<Route path="suppressions" element={<MaterialSyncSuppressionsPage/>}/>
|
||||
<Route path=":id" element ={<MaterialDetailPage/>}/>
|
||||
</Route>
|
||||
<Route path="/wiki" element={<WikiSearchPage/>}/>
|
||||
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
<Route path="/wiki/new" element={<WikiNewPage user={user}/>}/>
|
||||
<Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
|
||||
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
|
||||
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
|
||||
<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="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
|
||||
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
|
||||
<Route path="/tos" element={<TOSPage/>}/>
|
||||
<Route path="/gekanator" element={<GekanatorPage user={user}/>}/>
|
||||
<Route path="/more" element={<MorePage/>}/>
|
||||
<Route path="*" element={<NotFound/>}/>
|
||||
</Routes>)
|
||||
|
||||
if (animationMode === 'off')
|
||||
return routes
|
||||
if (pageLocale != null && pageLocale !== locale)
|
||||
return <GekanatorUnavailablePage/>
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{routes}
|
||||
<Suspense fallback={null}>
|
||||
<GekanatorPage user={user}/>
|
||||
</Suspense>)
|
||||
}
|
||||
|
||||
|
||||
export const pageRoutes = (
|
||||
user: User | null,
|
||||
setUser: Dispatch<SetStateAction<User | null>>,
|
||||
): PageRoute[] => [
|
||||
{ path: '', element: <LocaleRedirect to="/posts"/>,
|
||||
handle: { redirectTo: '/posts' } },
|
||||
{ path: 'posts', element: <PostListPage/>,
|
||||
handle: { preload: PostListPage.preload, switchLocale: switchPostFilterLocale,
|
||||
sitemap: ['static'] } },
|
||||
{ path: 'posts/new', element: <PostNewPage user={user}/>,
|
||||
handle: { preload: PostNewPage.preload, robots: 'noindex' } },
|
||||
{ path: 'posts/search', element: <PostSearchPage/>,
|
||||
handle: { preload: PostSearchPage.preload, switchLocale: switchPostFilterLocale } },
|
||||
{ path: 'posts/:id', element: <PostDetailRoute user={user}/>,
|
||||
handle: { preload: PostDetailPage.preload, sitemap: ['posts'] } },
|
||||
{ path: 'posts/changes', element: <PostHistoryPage/>,
|
||||
handle: { preload: PostHistoryPage.preload } },
|
||||
{ path: 'tags', element: <TagListPage/>,
|
||||
handle: { preload: TagListPage.preload, sitemap: ['static'] } },
|
||||
{ path: 'tags/:id', element: <TagDetailPage/>,
|
||||
handle: { preload: TagDetailPage.preload, sitemap: ['tags'] } },
|
||||
{ path: 'tags/:id/deerjikists', element: <DeerjikistDetailPage/>,
|
||||
handle: { preload: DeerjikistDetailPage.preload, sitemap: ['deerjikists'] } },
|
||||
{ path: 'tags/nico', element: <NicoTagListPage user={user}/>,
|
||||
handle: { preload: NicoTagListPage.preload, sitemap: ['static'] } },
|
||||
{ path: 'nico/tags', element: <NicoTagListPage user={user}/>,
|
||||
handle: { preload: NicoTagListPage.preload } },
|
||||
{ path: 'tags/changes', element: <TagHistoryPage/>,
|
||||
handle: { preload: TagHistoryPage.preload } },
|
||||
{ path: 'theatres/:id', element: <TheatreDetailPage user={user}/>,
|
||||
handle: { preload: TheatreDetailPage.preload, sitemap: ['theatres'] } },
|
||||
{ path: 'materials', element: <MaterialBasePage/>,
|
||||
handle: { preload: MaterialBasePage.preload }, children: [
|
||||
{ index: true, element: <MaterialListPage/>,
|
||||
handle: { preload: MaterialListPage.preload, sitemap: ['static'] } },
|
||||
{ path: 'changes', element: <MaterialHistoryPage/>,
|
||||
handle: { preload: MaterialHistoryPage.preload } },
|
||||
{ path: 'new', element: <MaterialNewPage/>,
|
||||
handle: { preload: MaterialNewPage.preload, robots: 'noindex' } },
|
||||
{ path: 'suppressions', element: <MaterialSyncSuppressionsPage/>,
|
||||
handle: { preload: MaterialSyncSuppressionsPage.preload,
|
||||
robots: 'noindex', authentication: 'user' } },
|
||||
{ path: ':id', element: <MaterialDetailPage/>,
|
||||
handle: { preload: MaterialDetailPage.preload, sitemap: ['materials'] } }] },
|
||||
{ path: 'wiki', element: <WikiSearchPage/>,
|
||||
handle: { preload: WikiSearchPage.preload, sitemap: ['static'] } },
|
||||
{ path: 'wiki/:title', element: <WikiDetailPage/>,
|
||||
handle: { preload: WikiDetailPage.preload, switchLocale: switchWikiLocale,
|
||||
sitemap: ['descriptions', 'tag_groups'] } },
|
||||
{ path: 'wiki/new', element: <WikiNewPage user={user}/>,
|
||||
handle: { preload: WikiNewPage.preload, robots: 'noindex' } },
|
||||
{ path: 'wiki/:id/edit', element: <WikiEditPage user={user}/>,
|
||||
handle: { preload: WikiEditPage.preload, robots: 'noindex',
|
||||
switchLocale: request => switchWikiLocale (request, 'edit') } },
|
||||
{ path: 'wiki/:id/diff', element: <WikiDiffPage/>,
|
||||
handle: { preload: WikiDiffPage.preload,
|
||||
switchLocale: request => switchWikiLocale (request, 'diff') } },
|
||||
{ path: 'wiki/changes', element: <WikiHistoryPage/>,
|
||||
handle: { preload: WikiHistoryPage.preload,
|
||||
switchLocale: request => switchWikiLocale (request, 'history') } },
|
||||
{ path: 'help', element: <HelpListPage/>,
|
||||
handle: { preload: HelpListPage.preload, sitemap: ['static'] } },
|
||||
{ path: 'help/changes', element: <HelpChangesPage/>,
|
||||
handle: { preload: HelpChangesPage.preload } },
|
||||
{ path: 'help/:identifier/diff', element: <HelpDiffPage/>,
|
||||
handle: { preload: HelpDiffPage.preload } },
|
||||
{ id: 'help.detail', path: 'help/:identifier', element: <HelpDetailPage/>,
|
||||
handle: { prepare: prepareHelpPage, sitemap: ['help'] } },
|
||||
{ path: 'users/settings', element: <SettingPage user={user} setUser={setUser}/>,
|
||||
handle: { preload: SettingPage.preload, robots: 'noindex', authentication: 'user' } },
|
||||
{ path: 'settings', element: <LocaleRedirect to="/users/settings"/>,
|
||||
handle: { redirectTo: '/users/settings' } },
|
||||
{ path: 'tos', element: <TOSPage/>,
|
||||
handle: { preload: TOSPage.preload, sitemap: ['static'] } },
|
||||
{ id: 'gekanator', path: 'gekanator', element: <GekanatorRoute user={user}/>,
|
||||
handle: { locale: 'ja', sitemap: ['static'], preload: locale => locale === 'ja'
|
||||
? GekanatorPage.preload () : GekanatorUnavailablePage.preload () } },
|
||||
{ path: 'more', element: <MorePage/>,
|
||||
handle: { preload: MorePage.preload, sitemap: ['static'] } },
|
||||
{ path: '*', element: <NotFound/>, handle: { notFound: true } }]
|
||||
|
||||
|
||||
const matchPageRoutes = (pathname: string) =>
|
||||
matchRoutes<PageRoute> (pageRoutes (null, () => { }), unlocalisedPath (pathname))
|
||||
|
||||
|
||||
export const preloadPageRoute = async (
|
||||
pathname: string,
|
||||
locale: LocaleCode,
|
||||
): Promise<void> => {
|
||||
await Promise.all ((matchPageRoutes (pathname) ?? [])
|
||||
.map (match => match.route.handle?.preload?.(locale)))
|
||||
}
|
||||
|
||||
|
||||
export const resolvePageRoute = (
|
||||
pathname: string,
|
||||
): RouteMatch<string, PageRoute> | null => {
|
||||
const match = matchPageRoutes (pathname)?.at (-1)
|
||||
return (match == null || match.route.handle?.notFound) ? null : match
|
||||
}
|
||||
|
||||
|
||||
const RouteTransitionWrapper = ({ user, setUser }: {
|
||||
user: User | null
|
||||
setUser: Dispatch<SetStateAction<User | null>> }) => {
|
||||
const routes = useRoutes (pageRoutes (user, setUser))
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<Suspense fallback={null}>{routes}</Suspense>
|
||||
</AnimatePresence>)
|
||||
}
|
||||
|
||||
@@ -128,12 +239,23 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
|
||||
}
|
||||
|
||||
|
||||
const RoutedApp: FC = () => {
|
||||
const [user, setUser] = useState<User | null> (null)
|
||||
const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
|
||||
const client = useQueryClient ()
|
||||
const location = useLocation ()
|
||||
const locale = useLocale ()
|
||||
const route = resolvePageRoute (location.pathname)
|
||||
const robots = route?.route.handle?.robots
|
||||
const switchLocale = async (targetLocale: LocaleCode): Promise<string> => {
|
||||
const resolve = route?.route.handle?.switchLocale
|
||||
return (resolve != null && route != null)
|
||||
? resolve ({ location, params: route.params, locale, targetLocale })
|
||||
: switchLocalePath (location, targetLocale)
|
||||
}
|
||||
const [user, setUser] = useState<User | null> (
|
||||
() => client.getQueryData<User> (sessionKeys.current) ?? null)
|
||||
const [status, setStatus] = useState (200)
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
const animationMode = behaviourSettings.animation ?? 'normal'
|
||||
const LayoutWrapper = animationMode === 'off' ? Fragment : LayoutGroup
|
||||
|
||||
const appLayoutTransition = useMemo (
|
||||
() => (
|
||||
@@ -164,36 +286,21 @@ const RoutedApp: FC = () => {
|
||||
}, [animationMode])
|
||||
|
||||
useEffect (() => {
|
||||
const createUser = async () => {
|
||||
const data = await apiPost<{ code: string; user: User }> ('/users')
|
||||
if (data.code)
|
||||
{
|
||||
localStorage.setItem ('user_code', data.code)
|
||||
setUser (data.user)
|
||||
}
|
||||
}
|
||||
|
||||
const code = localStorage.getItem ('user_code')
|
||||
if (code)
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
const data = await apiPost<{ valid: boolean; user: User }> ('/users/verify', { code })
|
||||
if (data.valid)
|
||||
setUser (data.user)
|
||||
else
|
||||
await createUser ()
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
if (isApiError (err))
|
||||
setStatus (err.response?.status ?? 200)
|
||||
}
|
||||
}) ()
|
||||
const user = await initialiseUserSession ()
|
||||
if (!cancelled)
|
||||
setUser (user)
|
||||
}
|
||||
else
|
||||
createUser ()
|
||||
catch (error)
|
||||
{
|
||||
if (!cancelled && isApiError (error))
|
||||
setStatus (error.response?.status ?? 503)
|
||||
}
|
||||
}) ()
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
useEffect (() => {
|
||||
@@ -228,6 +335,9 @@ const RoutedApp: FC = () => {
|
||||
|
||||
return (
|
||||
<DialogueProvider>
|
||||
<PageHead>
|
||||
{robots != null && <meta name="robots" content={robots}/>}
|
||||
</PageHead>
|
||||
<UnsavedChangesGuardProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<MotionConfig
|
||||
@@ -238,18 +348,17 @@ const RoutedApp: FC = () => {
|
||||
? 'user'
|
||||
: 'always'
|
||||
}>
|
||||
<LayoutWrapper>
|
||||
<LayoutGroup>
|
||||
<motion.div
|
||||
layout={animationMode === 'off' ? false : 'position'}
|
||||
transition={{ layout: appLayoutTransition }}
|
||||
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
|
||||
<TopNav user={user}/>
|
||||
<TopNav user={user} locales={locales} resolveLocaleSwitch={switchLocale}/>
|
||||
<RouteTransitionWrapper
|
||||
animationMode={animationMode}
|
||||
user={user}
|
||||
setUser={setUser}/>
|
||||
</motion.div>
|
||||
</LayoutWrapper>
|
||||
</LayoutGroup>
|
||||
</MotionConfig>
|
||||
|
||||
<Toaster/>
|
||||
@@ -259,17 +368,73 @@ const RoutedApp: FC = () => {
|
||||
}
|
||||
|
||||
|
||||
const router = createBrowserRouter ([{
|
||||
path: '*',
|
||||
element: <RoutedApp/> }])
|
||||
const LocaleRoute: FC = () => {
|
||||
const { locale } = useParams ()
|
||||
const location = useLocation ()
|
||||
const { i18n } = useTranslation ()
|
||||
const { data, isError } = useQuery ({
|
||||
queryKey: localesKeys.index,
|
||||
queryFn: fetchLocales,
|
||||
staleTime: 60_000,
|
||||
refetchInterval: 60_000,
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true })
|
||||
const activeLocale =
|
||||
(isLocaleCode (locale)
|
||||
&& data?.locales.some (entry => entry.code === locale))
|
||||
? locale
|
||||
: null
|
||||
|
||||
useLayoutEffect (() => {
|
||||
if (activeLocale != null)
|
||||
document.documentElement.lang = activeLocale
|
||||
if (activeLocale != null && i18n.language !== activeLocale)
|
||||
void i18n.changeLanguage (activeLocale)
|
||||
}, [activeLocale, i18n])
|
||||
|
||||
const redirect = legacyLocalePath (location)
|
||||
if (!(isLocaleCode (locale)) && redirect == null)
|
||||
return <NotFound/>
|
||||
if (isError && data == null)
|
||||
return <ServiceUnavailable/>
|
||||
if (data == null)
|
||||
return null
|
||||
if (isLocaleCode (locale))
|
||||
{
|
||||
return data.locales.some (entry => entry.code === locale)
|
||||
? <RoutedApp locales={data.locales}/>
|
||||
: <NotFound/>
|
||||
}
|
||||
|
||||
return redirect == null ? <NotFound/> : <Navigate to={redirect} replace/>
|
||||
}
|
||||
|
||||
|
||||
const App: FC = () => (
|
||||
const LocaleRedirect = ({ to }: { to: string }) => {
|
||||
const locale = useLocale ()
|
||||
const location = useLocation ()
|
||||
return <Navigate to={switchLocalePath ({ ...location, pathname: to }, locale)} replace/>
|
||||
}
|
||||
|
||||
|
||||
export const appRoutes = [
|
||||
{ path: '/:locale/*', element: <LocaleRoute/> },
|
||||
{ path: '*', element: <LocaleRoute/> }]
|
||||
|
||||
|
||||
export const AppFrame = ({ children }: { children: ReactNode }) => (
|
||||
<>
|
||||
<RouteBlockerOverlay/>
|
||||
{import.meta.env.DEV && <DevModeWatermark/>}
|
||||
<RouterProvider router={router}/>
|
||||
{children}
|
||||
</>)
|
||||
|
||||
|
||||
const App: FC = () => {
|
||||
const [router] = useState (() => createBrowserRouter (appRoutes))
|
||||
return <AppFrame><RouterProvider router={router}/></AppFrame>
|
||||
}
|
||||
|
||||
|
||||
export default App
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする