ファイル
btrc-hub/frontend/src/pages/GekanatorUnavailablePage.tsx
T
2026-10-20 20:44:14 +09:00

142 行
4.9 KiB
TypeScript

import { motion, useReducedMotion } from 'framer-motion'
import { useLayoutEffect, useRef } from 'react'
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 PageHead from '@/components/common/PageHead'
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">
<PageHead title={title}>
<meta name="robots" content="noindex"/>
<style>{`
@font-face {
font-family: 'Nikumaru';
src: url(${nikumaru}) format('opentype');
}
`}</style>
</PageHead>
<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