このコミットが含まれているのは:
+30
-5
@@ -1,3 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { createBrowserRouter,
|
||||
@@ -14,7 +15,8 @@ import TopNav from '@/components/TopNav'
|
||||
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { apiPost, isApiError } from '@/lib/api'
|
||||
import { isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
|
||||
import { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
|
||||
import { localesKeys } from '@/lib/queryKeys'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { applyClientAnimationMode,
|
||||
applyClientAppearance,
|
||||
@@ -65,6 +67,7 @@ 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'
|
||||
|
||||
|
||||
@@ -131,7 +134,7 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
|
||||
}
|
||||
|
||||
|
||||
const RoutedApp: FC = () => {
|
||||
const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
|
||||
const [user, setUser] = useState<User | null> (null)
|
||||
const [status, setStatus] = useState (200)
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
@@ -246,7 +249,7 @@ const RoutedApp: FC = () => {
|
||||
layout={animationMode === 'off' ? false : 'position'}
|
||||
transition={{ layout: appLayoutTransition }}
|
||||
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
|
||||
<TopNav user={user}/>
|
||||
<TopNav user={user} locales={locales}/>
|
||||
<RouteTransitionWrapper
|
||||
animationMode={animationMode}
|
||||
user={user}
|
||||
@@ -265,10 +268,32 @@ const RoutedApp: FC = () => {
|
||||
const LocaleRoute: FC = () => {
|
||||
const { locale } = useParams ()
|
||||
const location = useLocation ()
|
||||
if (isLocaleCode (locale))
|
||||
return <RoutedApp/>
|
||||
const { data, isPending, isError, refetch } = useQuery ({
|
||||
queryKey: localesKeys.index,
|
||||
queryFn: fetchLocales,
|
||||
staleTime: 0,
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: 'always',
|
||||
refetchOnReconnect: 'always' })
|
||||
|
||||
useEffect (() => {
|
||||
void refetch ()
|
||||
}, [location.key, refetch])
|
||||
|
||||
const redirect = legacyLocalePath (location)
|
||||
if (!(isLocaleCode (locale)) && redirect == null)
|
||||
return <NotFound/>
|
||||
if (isError)
|
||||
return <ServiceUnavailable/>
|
||||
if (isPending)
|
||||
return null
|
||||
if (isLocaleCode (locale))
|
||||
{
|
||||
return data.locales.some (entry => entry.code === locale)
|
||||
? <RoutedApp locales={data.locales}/>
|
||||
: <NotFound/>
|
||||
}
|
||||
|
||||
return redirect == null ? <NotFound/> : <Navigate to={redirect} replace/>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Globe } from 'lucide-react'
|
||||
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -8,9 +9,7 @@ import Separator from '@/components/MenuSeparator'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import TopNavUser from '@/components/TopNavUser'
|
||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||
import { LOCALE_LABELS,
|
||||
SUPPORTED_LOCALES,
|
||||
isLocaleCode,
|
||||
import { isLocaleCode,
|
||||
localeFromPath,
|
||||
switchLocalePath,
|
||||
unlocalisedPath } from '@/lib/locales'
|
||||
@@ -25,9 +24,10 @@ import { fetchWikiPage } from '@/lib/wiki'
|
||||
|
||||
import type { FC, MouseEvent } from 'react'
|
||||
|
||||
import type { AvailableLocale } from '@/lib/locales'
|
||||
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
type Props = { user: User | null; locales: AvailableLocale[] }
|
||||
type NavDensity = 'auto' | 'comfortable' | 'compact'
|
||||
|
||||
|
||||
@@ -116,8 +116,9 @@ export const menuOutline = (
|
||||
}
|
||||
|
||||
|
||||
const TopNav: FC<Props> = ({ user }) => {
|
||||
const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
const locale = useLocale ()
|
||||
const localeName = locales.find (entry => entry.code === locale)?.name
|
||||
const navigate = useLocaleNavigate ()
|
||||
const location = useLocation ()
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
@@ -229,7 +230,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
if (layout == null || bar == null || primary == null)
|
||||
return
|
||||
|
||||
const userLink = bar.querySelector<HTMLElement> ('.top-nav-user-desktop')
|
||||
const account = bar.querySelector<HTMLElement> ('.top-nav-account')
|
||||
let raf = 0
|
||||
let active = true
|
||||
|
||||
@@ -252,9 +253,9 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
layout.setAttribute ('data-density', candidate)
|
||||
const style = getComputedStyle (bar)
|
||||
const required = primary.getBoundingClientRect ().width
|
||||
+ (userLink?.getBoundingClientRect ().width ?? 0)
|
||||
+ (account?.getBoundingClientRect ().width ?? 0)
|
||||
+ parseFloat (style.paddingLeft) + parseFloat (style.paddingRight)
|
||||
+ (userLink == null ? 0 : parseFloat (style.columnGap))
|
||||
+ (account == null ? 0 : parseFloat (style.columnGap))
|
||||
if (required <= bar.clientWidth)
|
||||
return candidate
|
||||
}
|
||||
@@ -283,8 +284,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (schedule)
|
||||
observer?.observe (bar)
|
||||
observer?.observe (primary)
|
||||
if (userLink != null)
|
||||
observer?.observe (userLink)
|
||||
if (account != null)
|
||||
observer?.observe (account)
|
||||
update ()
|
||||
addEventListener ('resize', schedule)
|
||||
document.fonts?.addEventListener ('loadingdone', schedule)
|
||||
@@ -297,7 +298,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
removeEventListener ('resize', schedule)
|
||||
document.fonts?.removeEventListener ('loadingdone', schedule)
|
||||
}
|
||||
}, [activeIdx, moreVsbl, desktop, density, user?.name])
|
||||
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales])
|
||||
|
||||
useEffect (() => {
|
||||
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
||||
@@ -309,6 +310,32 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
setOpenItemIdx (activeIdx)
|
||||
}, [activeIdx, location])
|
||||
|
||||
const localeSelector = (
|
||||
<label className="top-nav-menu-link relative flex h-8 shrink-0 items-center gap-1
|
||||
rounded px-1 focus-within:ring-2">
|
||||
<Globe aria-hidden="true" size={16}/>
|
||||
<span className="top-nav-locale-name whitespace-nowrap text-sm">
|
||||
{localeName}
|
||||
</span>
|
||||
<select
|
||||
aria-label={localeName}
|
||||
value={locale}
|
||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||
onChange={event => {
|
||||
const nextLocale = event.target.value
|
||||
if (isLocaleCode (nextLocale)
|
||||
&& locales.some (entry => entry.code === nextLocale))
|
||||
{
|
||||
navigate (switchLocalePath (location, nextLocale))
|
||||
}
|
||||
}}>
|
||||
{locales.map (entry => isLocaleCode (entry.code) && (
|
||||
<option key={entry.code} value={entry.code}>
|
||||
{entry.name}
|
||||
</option>))}
|
||||
</select>
|
||||
</label>)
|
||||
|
||||
return (
|
||||
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
|
||||
data-desktop={desktop} data-density={density}>
|
||||
@@ -374,35 +401,25 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TopNavUser user={user}/>
|
||||
<div className="top-nav-account ml-auto flex min-w-0 items-center gap-2">
|
||||
<TopNavUser user={user}/>
|
||||
{localeSelector}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
|
||||
ml-auto border-0 bg-transparent pr-4"
|
||||
onClick={() => {
|
||||
setMenuOpen (!(menuOpen))
|
||||
}}>
|
||||
{menuOpen ? '×' : 'Menu'}
|
||||
</button>
|
||||
<div className="top-nav-mobile-controls ml-auto flex shrink-0 items-center gap-2">
|
||||
{menuOpen && localeSelector}
|
||||
<button
|
||||
type="button"
|
||||
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
|
||||
border-0 bg-transparent pr-4"
|
||||
onClick={() => {
|
||||
setMenuOpen (!(menuOpen))
|
||||
}}>
|
||||
{menuOpen ? '×' : 'Menu'}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="flex min-w-0 justify-end px-2">
|
||||
<select
|
||||
aria-label={LOCALE_LABELS[locale]}
|
||||
value={locale}
|
||||
className="max-w-full rounded border border-gray-300 bg-white text-xs text-gray-900
|
||||
dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||
onChange={event => {
|
||||
const nextLocale = event.target.value
|
||||
if (isLocaleCode (nextLocale))
|
||||
navigate (switchLocalePath (location, nextLocale))
|
||||
}}>
|
||||
{SUPPORTED_LOCALES.map (code => (
|
||||
<option key={code} value={code}>{LOCALE_LABELS[code]}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
<motion.div
|
||||
key="submenu-shell"
|
||||
|
||||
@@ -23,8 +23,7 @@ const TopNavUser: FC<Props> = ({ user, sp }) => {
|
||||
return (
|
||||
<>
|
||||
{sp && <Separator/>}
|
||||
<PrefetchLink to="/users/settings"
|
||||
className={className}>
|
||||
<PrefetchLink to="/users/settings" className={className}>
|
||||
{user.name || '名もなきニジラー'}
|
||||
</PrefetchLink>
|
||||
</>)
|
||||
|
||||
+31
-1
@@ -216,6 +216,7 @@ body
|
||||
|
||||
.top-nav-desktop-menu,
|
||||
.top-nav-desktop-submenu,
|
||||
.top-nav-layout .top-nav-account,
|
||||
.top-nav-user-desktop
|
||||
{
|
||||
display: none;
|
||||
@@ -242,8 +243,33 @@ body
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.top-nav-account .top-nav-user-desktop
|
||||
{
|
||||
min-width: 0;
|
||||
max-width: 24vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.top-nav-locale-name
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px)
|
||||
{
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-locale-name,
|
||||
.top-nav-layout[data-measuring] .top-nav-locale-name
|
||||
{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-account .top-nav-user-desktop,
|
||||
.top-nav-layout[data-measuring] .top-nav-account .top-nav-user-desktop
|
||||
{
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.top-nav-layout .top-nav-brand-link
|
||||
{
|
||||
font-size: 1.125rem;
|
||||
@@ -255,7 +281,9 @@ body
|
||||
}
|
||||
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-desktop-menu,
|
||||
.top-nav-layout[data-measuring] .top-nav-desktop-menu
|
||||
.top-nav-layout[data-measuring] .top-nav-desktop-menu,
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-account,
|
||||
.top-nav-layout[data-measuring] .top-nav-account
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
@@ -269,6 +297,8 @@ body
|
||||
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-mobile-toggle,
|
||||
.top-nav-layout[data-measuring] .top-nav-mobile-toggle,
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-mobile-controls,
|
||||
.top-nav-layout[data-measuring] .top-nav-mobile-controls,
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-mobile-menu,
|
||||
.top-nav-layout[data-desktop='true'] .top-nav-user-mobile
|
||||
{
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { To } from 'react-router-dom'
|
||||
|
||||
export const SUPPORTED_LOCALES = ['ja', 'ain-Kana'] as const
|
||||
|
||||
export type LocaleCode = typeof SUPPORTED_LOCALES[number]
|
||||
|
||||
export type AvailableLocale = { code: string; name: string }
|
||||
|
||||
|
||||
export const fetchLocales = (): Promise<{ locales: AvailableLocale[] }> =>
|
||||
apiGet ('/locales')
|
||||
|
||||
|
||||
export const LOCALE_LABELS: Record<LocaleCode, string> = {
|
||||
ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' }
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import type {
|
||||
MaterialFilter,
|
||||
} from '@/types'
|
||||
|
||||
export const localesKeys = {
|
||||
root: ['locales'] as const,
|
||||
index: ['locales', 'index'] as const }
|
||||
|
||||
export const postsKeys = {
|
||||
root: ['posts'] as const,
|
||||
index: (
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする