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