import { useQuery } from '@tanstack/react-query'
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
import { Fragment, Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { createBrowserRouter,
Navigate,
Route,
RouterProvider,
Routes,
useLocation,
useParams } 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 { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
import { requiredPageLocale } from '@/lib/routeLocales'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
fetchUserThemeSlots,
fetchUserSettings,
getClientThemeMode,
hasStoredClientThemeSelection,
normaliseUserThemeSlots,
setCachedUserThemeSlots,
seedClientThemeMode } from '@/lib/settings'
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 GekanatorUnavailablePage from '@/pages/GekanatorUnavailablePage'
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
import NotFound from '@/pages/NotFound'
import TOSPage from '@/pages/TOSPage'
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 { ClientAnimationMode } from '@/lib/settings'
import type { AvailableLocale } from '@/lib/locales'
import type { User } from '@/types'
const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage'))
const GekanatorRoute = ({ user }: { user: User | null }) => {
const locale = useLocale ()
const location = useLocation ()
const pageLocale = requiredPageLocale (location.pathname)
if (pageLocale != null && pageLocale !== locale)
return
return (
)
}
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
animationMode: ClientAnimationMode
user: User | null
setUser: Dispatch> }) => {
const location = useLocation ()
const routes = (
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
}/>
)
if (animationMode === 'off')
return routes
return (
{routes}
)
}
const PostDetailRoute = ({ user }: { user: User | null }) => {
const location = useLocation ()
const key = location.pathname
return
}
const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const [user, setUser] = useState (null)
const [status, setStatus] = useState (200)
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const LayoutWrapper = animationMode === 'off' ? Fragment : LayoutGroup
const appLayoutTransition = useMemo (
() => (
animationMode === 'off'
? { duration: 0 }
: (animationMode === 'reduced'
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const })
),
[animationMode],
)
useEffect (() => {
applyClientAppearance ()
const mediaQuery = window.matchMedia ('(prefers-color-scheme: dark)')
const handleThemeChange = () => {
if (getClientThemeMode () === 'system')
applyClientAppearance ()
}
mediaQuery.addEventListener ('change', handleThemeChange)
return () => mediaQuery.removeEventListener ('change', handleThemeChange)
}, [])
useEffect (() => {
applyClientAnimationMode (animationMode)
}, [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)
{
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)
}
}) ()
}
else
createUser ()
}, [])
useEffect (() => {
if (!(user))
return
void (async () => {
try
{
const [settings, themeSlots] = await Promise.all ([
fetchUserSettings (),
fetchUserThemeSlots (),
])
const storedThemeSlots = normaliseUserThemeSlots (themeSlots)
setCachedUserThemeSlots (storedThemeSlots)
if (!(hasStoredClientThemeSelection ()))
seedClientThemeMode (settings.theme)
applyClientAppearance ()
}
catch
{
return
}
}) ()
}, [user])
switch (status)
{
case 503:
return
}
return (
)
}
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const location = useLocation ()
const { i18n } = useTranslation ()
const { data, isPending, isError, refetch } = useQuery ({
queryKey: localesKeys.index,
queryFn: fetchLocales,
staleTime: 0,
refetchOnMount: 'always',
refetchOnWindowFocus: 'always',
refetchOnReconnect: 'always' })
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])
useEffect (() => {
void refetch ()
}, [location.key, refetch])
const redirect = legacyLocalePath (location)
if (!(isLocaleCode (locale)) && redirect == null)
return
if (isError)
return
if (isPending)
return null
if (isLocaleCode (locale))
{
return data.locales.some (entry => entry.code === locale)
?
:
}
return redirect == null ? :
}
const LocaleRedirect = ({ to }: { to: string }) => {
const locale = useLocale ()
const location = useLocation ()
return
}
const router = createBrowserRouter ([
{ path: '/:locale/*', element: },
{ path: '*', element: }])
const App: FC = () => (
<>
{import.meta.env.DEV && }
>)
export default App