diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 217c1ff..ebca5ea 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion' -import { Fragment, useEffect, useLayoutEffect, useMemo, useState } from 'react' +import { Fragment, Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { createBrowserRouter, Navigate, @@ -40,7 +40,7 @@ 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 GekanatorUnavailablePage from '@/pages/GekanatorUnavailablePage' import NicoTagListPage from '@/pages/tags/NicoTagListPage' import NotFound from '@/pages/NotFound' import TOSPage from '@/pages/TOSPage' @@ -72,6 +72,23 @@ 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 @@ -114,7 +131,7 @@ const RouteTransitionWrapper = ({ animationMode, user, setUser }: { }/> }/> }/> - }/> + }/> }/> }/> ) @@ -278,11 +295,9 @@ const LocaleRoute: FC = () => { refetchOnMount: 'always', refetchOnWindowFocus: 'always', refetchOnReconnect: 'always' }) - const pageLocale = requiredPageLocale (location.pathname) const activeLocale = (isLocaleCode (locale) - && data?.locales.some (entry => entry.code === locale) - && (pageLocale == null || pageLocale === locale)) + && data?.locales.some (entry => entry.code === locale)) ? locale : null @@ -304,12 +319,6 @@ const LocaleRoute: FC = () => { return if (isPending) return null - if (pageLocale != null && isLocaleCode (locale) && pageLocale !== locale) - { - return data.locales.some (entry => entry.code === pageLocale) - ? - : - } if (isLocaleCode (locale)) { return data.locales.some (entry => entry.code === locale) diff --git a/frontend/src/locales/ain-Kana/gekanator.json b/frontend/src/locales/ain-Kana/gekanator.json new file mode 100644 index 0000000..f76116d --- /dev/null +++ b/frontend/src/locales/ain-Kana/gekanator.json @@ -0,0 +1,8 @@ +{ + "unavailable": { + "title": "Gekanator タネ シサㇺ イタㇰ アニ パテㇰ エシノッ エアㇱカイ!", + "back": "ホㇱキ ソㇱ ウン ホㇱピ", + "switchToJapanese": "シサㇺ イタㇰ ウン シンナ", + "imageAlt": "ポロンノ チㇱ コㇿ アン 洗澡鹿" + } +} diff --git a/frontend/src/locales/ja/gekanator.json b/frontend/src/locales/ja/gekanator.json new file mode 100644 index 0000000..b211394 --- /dev/null +++ b/frontend/src/locales/ja/gekanator.json @@ -0,0 +1,8 @@ +{ + "unavailable": { + "title": "グカネータは現在日本語のみでプレー可能です!", + "back": "前の画面に戻る", + "switchToJapanese": "日本語に切替へる", + "imageAlt": "号泣している洗澡鹿" + } +} diff --git a/frontend/src/pages/GekanatorUnavailablePage.tsx b/frontend/src/pages/GekanatorUnavailablePage.tsx new file mode 100644 index 0000000..0e3c12c --- /dev/null +++ b/frontend/src/pages/GekanatorUnavailablePage.tsx @@ -0,0 +1,143 @@ +import { motion, useReducedMotion } from 'framer-motion' +import { useLayoutEffect, useRef } from 'react' +import { Helmet } from 'react-helmet-async' +import { useLocation } from 'react-router-dom' + +import nikumaru from '@/assets/fonts/nikumaru.otf' +import { formatUIString } from '@/components/LocalisedText' +import PrefetchLink from '@/components/PrefetchLink' +import MainArea from '@/components/layout/MainArea' +import { Button } from '@/components/ui/button' +import { SITE_TITLE } from '@/config' +import { switchLocalePath } from '@/lib/locales' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' +import { useLocaleNavigate } from '@/lib/useLocale' +import { useUITranslation } from '@/lib/useUITranslation' + + +const GekanatorUnavailablePage = () => { + const location = useLocation () + const navigate = useLocaleNavigate () + const behaviourSettings = useClientBehaviourSettings () + const prefersReducedMotion = useReducedMotion () + const shakeMascot = (behaviourSettings.animation ?? 'normal') === 'normal' + && !prefersReducedMotion + const { message, label } = useUITranslation ('gekanator') + const title = message ('unavailable.title') + const imageAlt = message ('unavailable.imageAlt') + const headingRef = useRef (null) + + useLayoutEffect (() => { + const heading = headingRef.current + const context = document.createElement ('canvas').getContext ('2d') + if (heading == null || context == null) + return + + const fitTitle = () => { + heading.style.fontSize = '' + heading.style.maxWidth = '' + const style = window.getComputedStyle (heading) + const minimum = Number.parseFloat (style.fontSize) + context.font = `${style.fontWeight} ${minimum}px ${style.fontFamily}` + const width = context.measureText (formatUIString (title.text, title.language)).width + if (width > 0) + { + const available = heading.clientWidth + const fitted = minimum * Math.max (0, available - 1) / width + const fontSize = Math.max (minimum, fitted) + heading.style.fontSize = `${fontSize}px` + let lower = available / 2 + let upper = available + // Keep the one-line font size and find a width that allows two lines. + for (let step = 0; step < 12; ++step) + { + const candidate = (lower + upper) / 2 + heading.style.maxWidth = `${candidate}px` + if (heading.scrollHeight <= fontSize * 1.25 * 2 + 1 + && heading.scrollWidth <= heading.clientWidth) + upper = candidate + else + lower = candidate + } + heading.style.maxWidth = `${Math.ceil (upper)}px` + } + } + + let disposed = false + const resizeObserver = new ResizeObserver (fitTitle) + resizeObserver.observe (heading) + window.addEventListener ('resize', fitTitle) + document.fonts.addEventListener ('loadingdone', fitTitle) + void document.fonts.ready.then (() => { + if (!disposed) + fitTitle () + }) + fitTitle () + + return () => { + disposed = true + resizeObserver.disconnect () + window.removeEventListener ('resize', fitTitle) + document.fonts.removeEventListener ('loadingdone', fitTitle) + } + }, [title.text, title.language]) + + const goBack = () => { + // The router index excludes unrelated external history and is zero on direct entry. + const index: unknown = window.history.state?.idx + if (typeof index === 'number' && index > 0) + navigate (-1) + else + navigate ('/posts', { replace: true }) + } + + return ( + + + {title.text} | {SITE_TITLE} + + + +
+

+ {label ('unavailable.title')} +

+ +
+ + +
+
+
) +} + + +export default GekanatorUnavailablePage