|
|
|
@@ -1,7 +1,7 @@ |
|
|
|
import axios from 'axios' |
|
|
|
import toCamel from 'camelcase-keys' |
|
|
|
import { AnimatePresence, motion } from 'framer-motion' |
|
|
|
import { Fragment, useState, useEffect } from 'react' |
|
|
|
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' |
|
|
|
import { Link, useLocation } from 'react-router-dom' |
|
|
|
|
|
|
|
import Separator from '@/components/MenuSeparator' |
|
|
|
@@ -20,6 +20,28 @@ type Props = { user: User | null } |
|
|
|
export default (({ user }: Props) => { |
|
|
|
const location = useLocation () |
|
|
|
|
|
|
|
const dirRef = useRef<(-1) | 1> (1) |
|
|
|
const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) |
|
|
|
const navRef = useRef<HTMLDivElement | null> (null) |
|
|
|
|
|
|
|
const measure = () => { |
|
|
|
const nav = navRef.current |
|
|
|
const el = itemsRef.current[activeIdx] |
|
|
|
if (!(nav) || !(el) || activeIdx < 0) |
|
|
|
return |
|
|
|
|
|
|
|
const navRect = nav.getBoundingClientRect () |
|
|
|
const elRect = el.getBoundingClientRect () |
|
|
|
|
|
|
|
setHl ({ left: elRect.left - navRect.left, |
|
|
|
width: elRect.width, |
|
|
|
visible: true }) |
|
|
|
} |
|
|
|
|
|
|
|
const [hl, setHl] = useState<{ left: number; width: number; visible: boolean }> ({ |
|
|
|
left: 0, |
|
|
|
width: 0, |
|
|
|
visible: false }) |
|
|
|
const [menuOpen, setMenuOpen] = useState (false) |
|
|
|
const [openItemIdx, setOpenItemIdx] = useState (-1) |
|
|
|
const [postCount, setPostCount] = useState<number | null> (null) |
|
|
|
@@ -54,6 +76,32 @@ export default (({ user }: Props) => { |
|
|
|
{ name: 'お前', to: `/users/${ user?.id }`, visible: false }, |
|
|
|
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }] |
|
|
|
|
|
|
|
const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to)) |
|
|
|
|
|
|
|
const prevActiveIdxRef = useRef<number> (activeIdx) |
|
|
|
|
|
|
|
if (activeIdx !== prevActiveIdxRef.current) |
|
|
|
{ |
|
|
|
dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 |
|
|
|
prevActiveIdxRef.current = activeIdx |
|
|
|
} |
|
|
|
|
|
|
|
const dir = dirRef.current |
|
|
|
|
|
|
|
useLayoutEffect (() => { |
|
|
|
if (activeIdx < 0) |
|
|
|
return |
|
|
|
|
|
|
|
const raf = requestAnimationFrame (measure) |
|
|
|
const onResize = () => requestAnimationFrame (measure) |
|
|
|
|
|
|
|
addEventListener ('resize', onResize) |
|
|
|
return () => { |
|
|
|
cancelAnimationFrame (raf) |
|
|
|
removeEventListener ('resize', onResize) |
|
|
|
} |
|
|
|
}, [activeIdx]) |
|
|
|
|
|
|
|
useEffect (() => { |
|
|
|
const unsubscribe = WikiIdBus.subscribe (setWikiId) |
|
|
|
return () => unsubscribe () |
|
|
|
@@ -99,16 +147,26 @@ export default (({ user }: Props) => { |
|
|
|
ぼざクリ タグ広場 |
|
|
|
</Link> |
|
|
|
|
|
|
|
{menu.map ((item, i) => ( |
|
|
|
<Link key={i} |
|
|
|
to={item.to} |
|
|
|
className={cn ('hidden md:flex h-full items-center', |
|
|
|
(location.pathname.startsWith (item.base || item.to) |
|
|
|
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold' |
|
|
|
: 'px-2'))}> |
|
|
|
{item.name} |
|
|
|
</Link> |
|
|
|
))} |
|
|
|
<div ref={navRef} className="relative hidden md:flex h-full items-center"> |
|
|
|
<div aria-hidden |
|
|
|
className={cn ('absolute top-1/2 -translate-y-1/2 h-full', |
|
|
|
'bg-yellow-200 dark:bg-red-950', |
|
|
|
'transition-[transform,width] duration-200 ease-out')} |
|
|
|
style={{ width: hl.width, |
|
|
|
transform: `translate(${ hl.left }px, -50%)`, |
|
|
|
opacity: hl.visible ? 1 : 0 }}/> |
|
|
|
|
|
|
|
{menu.map ((item, i) => ( |
|
|
|
<Link key={i} |
|
|
|
to={item.to} |
|
|
|
ref={el => { |
|
|
|
itemsRef.current[i] = el |
|
|
|
}} |
|
|
|
className={cn ('relative z-10 flex h-full items-center px-5', |
|
|
|
(i === openItemIdx) && 'font-bold')}> |
|
|
|
{item.name} |
|
|
|
</Link>))} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<TopNavUser user={user}/> |
|
|
|
@@ -125,16 +183,33 @@ export default (({ user }: Props) => { |
|
|
|
</a> |
|
|
|
</nav> |
|
|
|
|
|
|
|
<div className="hidden md:flex bg-yellow-200 dark:bg-red-950 |
|
|
|
items-center w-full min-h-[40px] px-3"> |
|
|
|
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu |
|
|
|
.filter (item => item.visible ?? true) |
|
|
|
.map ((item, i) => 'component' in item ? item.component : ( |
|
|
|
<Link key={i} |
|
|
|
to={item.to} |
|
|
|
className="h-full flex items-center px-3"> |
|
|
|
{item.name} |
|
|
|
</Link>))} |
|
|
|
<div className="relative hidden md:flex bg-yellow-200 dark:bg-red-950 |
|
|
|
items-center w-full min-h-[40px] overflow-hidden"> |
|
|
|
<AnimatePresence initial={false} custom={dir}> |
|
|
|
<motion.div |
|
|
|
key={activeIdx} |
|
|
|
custom={dir} |
|
|
|
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }), |
|
|
|
centre: { y: 0, opacity: 1 }, |
|
|
|
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} |
|
|
|
className="absolute inset-0 flex items-center px-3" |
|
|
|
initial="enter" |
|
|
|
animate="centre" |
|
|
|
exit="exit" |
|
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
|
{(menu[activeIdx]?.subMenu ?? []) |
|
|
|
.filter (item => item.visible ?? true) |
|
|
|
.map ((item, i) => ( |
|
|
|
'component' in item |
|
|
|
? <Fragment key={`c-${ i }`}>{item.component}</Fragment> |
|
|
|
: ( |
|
|
|
<Link key={`l-${ i }`} |
|
|
|
to={item.to} |
|
|
|
className="h-full flex items-center px-3"> |
|
|
|
{item.name} |
|
|
|
</Link>)))} |
|
|
|
</motion.div> |
|
|
|
</AnimatePresence> |
|
|
|
</div> |
|
|
|
|
|
|
|
<AnimatePresence initial={false}> |
|
|
|
@@ -167,16 +242,38 @@ export default (({ user }: Props) => { |
|
|
|
}}> |
|
|
|
{item.name} |
|
|
|
</Link> |
|
|
|
{i === openItemIdx && ( |
|
|
|
item.subMenu |
|
|
|
.filter (subItem => subItem.visible ?? true) |
|
|
|
.map ((subItem, j) => 'component' in subItem ? subItem.component : ( |
|
|
|
<Link key={j} |
|
|
|
to={subItem.to} |
|
|
|
className="w-full min-h-[36px] flex items-center pl-12 |
|
|
|
bg-yellow-50 dark:bg-red-950"> |
|
|
|
{subItem.name} |
|
|
|
</Link>)))} |
|
|
|
|
|
|
|
<AnimatePresence initial={false}> |
|
|
|
{i === openItemIdx && ( |
|
|
|
<motion.div |
|
|
|
key={`sp-sub-${ i }`} |
|
|
|
className="w-full bg-yellow-50 dark:bg-red-950" |
|
|
|
variants={{ closed: { clipPath: 'inset(0 0 100% 0)', |
|
|
|
height: 0, |
|
|
|
opacity: 0 }, |
|
|
|
open: { clipPath: 'inset(0 0 0% 0)', |
|
|
|
height: 'auto', |
|
|
|
opacity: 1 } }} |
|
|
|
initial="closed" |
|
|
|
animate="open" |
|
|
|
exit="closed" |
|
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
|
{item.subMenu |
|
|
|
.filter (subItem => subItem.visible ?? true) |
|
|
|
.map ((subItem, j) => ( |
|
|
|
'component' in subItem |
|
|
|
? ( |
|
|
|
<Fragment key={`sp-c-${ i }-${ j }`}> |
|
|
|
{subItem.component} |
|
|
|
</Fragment>) |
|
|
|
: ( |
|
|
|
<Link key={`sp-l-${ i }-${ j }`} |
|
|
|
to={subItem.to} |
|
|
|
className="w-full min-h-[36px] flex items-center pl-12"> |
|
|
|
{subItem.name} |
|
|
|
</Link>)))} |
|
|
|
</motion.div>)} |
|
|
|
</AnimatePresence> |
|
|
|
</Fragment>))} |
|
|
|
<TopNavUser user={user} sp/> |
|
|
|
<Separator/> |
|
|
|
|