このコミットが含まれているのは:
+21
-12
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { createBrowserRouter,
|
import { createBrowserRouter,
|
||||||
Navigate,
|
Navigate,
|
||||||
@@ -40,7 +40,7 @@ import MaterialListPage from '@/pages/materials/MaterialListPage'
|
|||||||
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
||||||
import MaterialSyncSuppressionsPage from '@/pages/materials/MaterialSyncSuppressionsPage'
|
import MaterialSyncSuppressionsPage from '@/pages/materials/MaterialSyncSuppressionsPage'
|
||||||
import MorePage from '@/pages/MorePage'
|
import MorePage from '@/pages/MorePage'
|
||||||
import GekanatorPage from '@/pages/GekanatorPage'
|
import GekanatorUnavailablePage from '@/pages/GekanatorUnavailablePage'
|
||||||
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
import TOSPage from '@/pages/TOSPage'
|
import TOSPage from '@/pages/TOSPage'
|
||||||
@@ -72,6 +72,23 @@ import type { ClientAnimationMode } from '@/lib/settings'
|
|||||||
import type { AvailableLocale } from '@/lib/locales'
|
import type { AvailableLocale } from '@/lib/locales'
|
||||||
import type { User } from '@/types'
|
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 <GekanatorUnavailablePage/>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<GekanatorPage user={user}/>
|
||||||
|
</Suspense>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
||||||
animationMode: ClientAnimationMode
|
animationMode: ClientAnimationMode
|
||||||
@@ -114,7 +131,7 @@ const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
|
|||||||
<Route path="users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
|
<Route path="users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
|
||||||
<Route path="settings" element={<LocaleRedirect to="/users/settings"/>}/>
|
<Route path="settings" element={<LocaleRedirect to="/users/settings"/>}/>
|
||||||
<Route path="tos" element={<TOSPage/>}/>
|
<Route path="tos" element={<TOSPage/>}/>
|
||||||
<Route path="gekanator" element={<GekanatorPage user={user}/>}/>
|
<Route path="gekanator" element={<GekanatorRoute user={user}/>}/>
|
||||||
<Route path="more" element={<MorePage/>}/>
|
<Route path="more" element={<MorePage/>}/>
|
||||||
<Route path="*" element={<NotFound/>}/>
|
<Route path="*" element={<NotFound/>}/>
|
||||||
</Routes>)
|
</Routes>)
|
||||||
@@ -278,11 +295,9 @@ const LocaleRoute: FC = () => {
|
|||||||
refetchOnMount: 'always',
|
refetchOnMount: 'always',
|
||||||
refetchOnWindowFocus: 'always',
|
refetchOnWindowFocus: 'always',
|
||||||
refetchOnReconnect: 'always' })
|
refetchOnReconnect: 'always' })
|
||||||
const pageLocale = requiredPageLocale (location.pathname)
|
|
||||||
const activeLocale =
|
const activeLocale =
|
||||||
(isLocaleCode (locale)
|
(isLocaleCode (locale)
|
||||||
&& data?.locales.some (entry => entry.code === locale)
|
&& data?.locales.some (entry => entry.code === locale))
|
||||||
&& (pageLocale == null || pageLocale === locale))
|
|
||||||
? locale
|
? locale
|
||||||
: null
|
: null
|
||||||
|
|
||||||
@@ -304,12 +319,6 @@ const LocaleRoute: FC = () => {
|
|||||||
return <ServiceUnavailable/>
|
return <ServiceUnavailable/>
|
||||||
if (isPending)
|
if (isPending)
|
||||||
return null
|
return null
|
||||||
if (pageLocale != null && isLocaleCode (locale) && pageLocale !== locale)
|
|
||||||
{
|
|
||||||
return data.locales.some (entry => entry.code === pageLocale)
|
|
||||||
? <Navigate to={switchLocalePath (location, pageLocale)} replace/>
|
|
||||||
: <NotFound/>
|
|
||||||
}
|
|
||||||
if (isLocaleCode (locale))
|
if (isLocaleCode (locale))
|
||||||
{
|
{
|
||||||
return data.locales.some (entry => entry.code === locale)
|
return data.locales.some (entry => entry.code === locale)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"unavailable": {
|
||||||
|
"title": "Gekanator タネ シサㇺ イタㇰ アニ パテㇰ エシノッ エアㇱカイ!",
|
||||||
|
"back": "ホㇱキ ソㇱ ウン ホㇱピ",
|
||||||
|
"switchToJapanese": "シサㇺ イタㇰ ウン シンナ",
|
||||||
|
"imageAlt": "ポロンノ チㇱ コㇿ アン 洗澡鹿"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"unavailable": {
|
||||||
|
"title": "グカネータは現在日本語のみでプレー可能です!",
|
||||||
|
"back": "前の画面に戻る",
|
||||||
|
"switchToJapanese": "日本語に切替へる",
|
||||||
|
"imageAlt": "号泣している洗澡鹿"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HTMLHeadingElement> (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 (
|
||||||
|
<MainArea className="min-w-0">
|
||||||
|
<Helmet>
|
||||||
|
<title>{title.text} | {SITE_TITLE}</title>
|
||||||
|
<meta name="robots" content="noindex"/>
|
||||||
|
<style>{`
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nikumaru';
|
||||||
|
src: url(${nikumaru}) format('opentype');
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</Helmet>
|
||||||
|
<section className="mx-auto flex min-h-full w-full flex-col
|
||||||
|
items-center justify-center gap-6 py-6 text-center">
|
||||||
|
<h1 ref={headingRef} className="w-full text-5xl font-bold"
|
||||||
|
style={{ fontFamily: 'Nikumaru', lineHeight: 1.25 }}>
|
||||||
|
{label ('unavailable.title')}
|
||||||
|
</h1>
|
||||||
|
<motion.img
|
||||||
|
src="/assets/gekanator/mascot-failed.png"
|
||||||
|
alt={imageAlt.text}
|
||||||
|
lang={imageAlt.language}
|
||||||
|
width={1179}
|
||||||
|
height={1179}
|
||||||
|
animate={shakeMascot
|
||||||
|
? { x: [0, -10, 10, -8, 8, 0], y: [0, 8, -8, 10, -10, 0], rotate: 0 }
|
||||||
|
: { x: 0, y: 0, rotate: 0 }}
|
||||||
|
transition={{ duration: shakeMascot ? .16 : 0,
|
||||||
|
repeat: shakeMascot ? Infinity : 0, ease: 'linear' }}
|
||||||
|
className="h-auto max-h-[50dvh] w-full max-w-lg object-contain"/>
|
||||||
|
<div className="flex w-full flex-col items-stretch justify-center gap-3 md:flex-row">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={goBack}
|
||||||
|
className="h-auto min-h-10 min-w-0 whitespace-normal">
|
||||||
|
{label ('unavailable.back')}
|
||||||
|
</Button>
|
||||||
|
<Button asChild className="h-auto min-h-10 min-w-0 whitespace-normal">
|
||||||
|
<PrefetchLink to={switchLocalePath (location, 'ja')}>
|
||||||
|
{label ('unavailable.switchToJapanese')}
|
||||||
|
</PrefetchLink>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</MainArea>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default GekanatorUnavailablePage
|
||||||
新しいイシューから参照
ユーザーをブロックする