|
|
@@ -19,13 +19,15 @@ import type { Menu, MenuVisibleItem, Tag, User } from '@/types' |
|
|
type Props = { user: User | null } |
|
|
type Props = { user: User | null } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const menuOutline = ({ tag, wikiId, user }: { tag?: Tag | null |
|
|
|
|
|
wikiId: number | null |
|
|
|
|
|
user: User | null }): Menu => { |
|
|
|
|
|
|
|
|
export const menuOutline = ({ tag, wikiId, user, pathName }: { |
|
|
|
|
|
tag?: Tag | null |
|
|
|
|
|
wikiId: number | null |
|
|
|
|
|
user: User | null, |
|
|
|
|
|
pathName: string }): Menu => { |
|
|
const postCount = tag?.postCount ?? 0 |
|
|
const postCount = tag?.postCount ?? 0 |
|
|
|
|
|
|
|
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) |
|
|
|
|
|
const wikiTitle = location.pathname.split ('/')[2] ?? '' |
|
|
|
|
|
|
|
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (pathName) && wikiId) |
|
|
|
|
|
const wikiTitle = pathName.split ('/')[2] ?? '' |
|
|
|
|
|
|
|
|
return [ |
|
|
return [ |
|
|
{ name: '広場', to: '/posts', subMenu: [ |
|
|
{ name: '広場', to: '/posts', subMenu: [ |
|
|
@@ -80,7 +82,7 @@ export default (({ user }: Props) => { |
|
|
|
|
|
|
|
|
const measure = (idx: number) => { |
|
|
const measure = (idx: number) => { |
|
|
const nav = navRef.current |
|
|
const nav = navRef.current |
|
|
const el = itemsRef.current[idx < 0 ? menu.length : idx] |
|
|
|
|
|
|
|
|
const el = itemsRef.current[idx < 0 ? visibleMenu.length : idx] |
|
|
|
|
|
|
|
|
if (!(nav) || !(el)) |
|
|
if (!(nav) || !(el)) |
|
|
{ |
|
|
{ |
|
|
@@ -119,10 +121,10 @@ export default (({ user }: Props) => { |
|
|
queryKey: tagsKeys.show (effectiveTitle), |
|
|
queryKey: tagsKeys.show (effectiveTitle), |
|
|
queryFn: () => fetchTagByName (effectiveTitle) }) |
|
|
queryFn: () => fetchTagByName (effectiveTitle) }) |
|
|
|
|
|
|
|
|
const menu = menuOutline ({ tag, wikiId, user }) |
|
|
|
|
|
const activeIdx = (menu |
|
|
|
|
|
.filter ((item): item is MenuVisibleItem => item.visible ?? true) |
|
|
|
|
|
.findIndex (item => location.pathname.startsWith (item.base || item.to))) |
|
|
|
|
|
|
|
|
const menu = menuOutline ({ tag, wikiId, user, pathName: location.pathname }) |
|
|
|
|
|
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) |
|
|
|
|
|
const activeIdx = |
|
|
|
|
|
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to)) |
|
|
|
|
|
|
|
|
const prevActiveIdxRef = useRef<number> (activeIdx) |
|
|
const prevActiveIdxRef = useRef<number> (activeIdx) |
|
|
|
|
|
|
|
|
@@ -135,26 +137,24 @@ export default (({ user }: Props) => { |
|
|
const dir = dirRef.current |
|
|
const dir = dirRef.current |
|
|
|
|
|
|
|
|
useLayoutEffect (() => { |
|
|
useLayoutEffect (() => { |
|
|
const raf = requestAnimationFrame (() => measure (activeIdx)) |
|
|
|
|
|
const onResize = () => requestAnimationFrame (() => measure (activeIdx)) |
|
|
|
|
|
|
|
|
const raf = requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) |
|
|
|
|
|
const onResize = () => requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) |
|
|
|
|
|
|
|
|
addEventListener ('resize', onResize) |
|
|
addEventListener ('resize', onResize) |
|
|
return () => { |
|
|
return () => { |
|
|
cancelAnimationFrame (raf) |
|
|
cancelAnimationFrame (raf) |
|
|
removeEventListener ('resize', onResize) |
|
|
removeEventListener ('resize', onResize) |
|
|
} |
|
|
} |
|
|
}, [activeIdx]) |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
useEffect (() => { |
|
|
useEffect (() => { |
|
|
const unsubscribe = WikiIdBus.subscribe (setWikiId) |
|
|
const unsubscribe = WikiIdBus.subscribe (setWikiId) |
|
|
return () => unsubscribe () |
|
|
return () => unsubscribe () |
|
|
}, []) |
|
|
|
|
|
|
|
|
}, [activeIdx]) |
|
|
|
|
|
|
|
|
useEffect (() => { |
|
|
useEffect (() => { |
|
|
setMenuOpen (false) |
|
|
setMenuOpen (false) |
|
|
setOpenItemIdx (menu |
|
|
|
|
|
.filter ((item): item is MenuVisibleItem => item.visible ?? true) |
|
|
|
|
|
.findIndex (item => location.pathname.startsWith (item.base || item.to))) |
|
|
|
|
|
|
|
|
setOpenItemIdx (activeIdx) |
|
|
}, [location]) |
|
|
}, [location]) |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
@@ -181,30 +181,29 @@ export default (({ user }: Props) => { |
|
|
transform: `translateX(${ hl.left }px)`, |
|
|
transform: `translateX(${ hl.left }px)`, |
|
|
opacity: hl.visible ? 1 : 0 }}/> |
|
|
opacity: hl.visible ? 1 : 0 }}/> |
|
|
|
|
|
|
|
|
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) |
|
|
|
|
|
.map ((item, i) => ( |
|
|
|
|
|
|
|
|
{visibleMenu.map ((item, i) => ( |
|
|
<motion.div |
|
|
<motion.div |
|
|
key={item.to} |
|
|
key={item.to} |
|
|
layoutId={`menu-${ item.name }`} |
|
|
layoutId={`menu-${ item.name }`} |
|
|
onMouseEnter={() => { |
|
|
|
|
|
setMoreVsbl (false) |
|
|
|
|
|
setTimeout (() => measure (activeIdx), 300) |
|
|
|
|
|
}}> |
|
|
|
|
|
|
|
|
animate={{ opacity: moreVsbl ? 0 : 1 }} |
|
|
|
|
|
transition={{ opacity: { duration: .12 }, |
|
|
|
|
|
layout: { duration: .2, ease: 'easeOut' } }} |
|
|
|
|
|
style={{ pointerEvents: moreVsbl ? 'none' : 'auto' }} |
|
|
|
|
|
onMouseEnter={() => setMoreVsbl (false)}> |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to={item.to} |
|
|
to={item.to} |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
itemsRef.current[i] = el |
|
|
itemsRef.current[i] = el |
|
|
}} |
|
|
}} |
|
|
className={cn ('relative z-10 flex h-full items-center px-5', |
|
|
className={cn ('relative z-10 flex h-full items-center px-5', |
|
|
(i === openItemIdx) && 'font-bold', |
|
|
|
|
|
moreVsbl && 'pointer-events-none')}> |
|
|
|
|
|
|
|
|
(i === openItemIdx) && 'font-bold')}> |
|
|
{item.name} |
|
|
{item.name} |
|
|
</PrefetchLink> |
|
|
</PrefetchLink> |
|
|
</motion.div>))} |
|
|
</motion.div>))} |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to="#" |
|
|
to="#" |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
itemsRef.current[menu.length] = el |
|
|
|
|
|
|
|
|
itemsRef.current[visibleMenu.length] = el |
|
|
}} |
|
|
}} |
|
|
onClick={e => e.preventDefault ()} |
|
|
onClick={e => e.preventDefault ()} |
|
|
onMouseEnter={() => { |
|
|
onMouseEnter={() => { |
|
|
@@ -235,33 +234,33 @@ export default (({ user }: Props) => { |
|
|
<AnimatePresence initial={false}> |
|
|
<AnimatePresence initial={false}> |
|
|
<motion.div |
|
|
<motion.div |
|
|
key="submenu-shell" |
|
|
key="submenu-shell" |
|
|
|
|
|
layout |
|
|
className="relative hidden md:block overflow-hidden |
|
|
className="relative hidden md:block overflow-hidden |
|
|
bg-yellow-200 dark:bg-red-950" |
|
|
bg-yellow-200 dark:bg-red-950" |
|
|
|
|
|
style={{ height: moreVsbl ? 40 * menu.length : (activeIdx < 0 ? 0 : 40) }} |
|
|
onMouseLeave={() => { |
|
|
onMouseLeave={() => { |
|
|
if (!(moreVsbl)) |
|
|
|
|
|
return |
|
|
|
|
|
setMoreVsbl (false) |
|
|
|
|
|
setTimeout (() => measure (activeIdx), 300) |
|
|
|
|
|
|
|
|
if (moreVsbl) |
|
|
|
|
|
setMoreVsbl (false) |
|
|
}} |
|
|
}} |
|
|
initial={{ height: 0 }} |
|
|
|
|
|
animate={{ height: (moreVsbl ? 40 * menu.length : (activeIdx < 0 ? 0 : 40)) }} |
|
|
|
|
|
exit={{ height: 0 }} |
|
|
|
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
|
|
|
|
|
|
transition={{ layout: { duration: .2, ease: 'easeOut' } }} |
|
|
|
|
|
onAnimationComplete={() => { |
|
|
|
|
|
measure (moreVsbl ? -1 : activeIdx) |
|
|
|
|
|
}}> |
|
|
{moreVsbl |
|
|
{moreVsbl |
|
|
? ( |
|
|
? ( |
|
|
menu.map ((item, i) => ( |
|
|
menu.map ((item, i) => ( |
|
|
<div key={i} className="relative h-[40px]"> |
|
|
<div key={i} className="relative h-[40px]"> |
|
|
<div className="absolute inset-0 flex items-center px-3"> |
|
|
<div className="absolute inset-0 flex items-center px-3"> |
|
|
<motion.h2 |
|
|
|
|
|
|
|
|
<motion.div |
|
|
|
|
|
transition={{ duration: .2, ease: 'easeOut' }} |
|
|
{...((item.visible ?? true) |
|
|
{...((item.visible ?? true) |
|
|
? { layoutId: `menu-${ item.name }` } |
|
|
? { layoutId: `menu-${ item.name }` } |
|
|
: { initial: { y: -40, opacity: 0 }, |
|
|
|
|
|
animate: { y: 0, opacity: 1 }, |
|
|
|
|
|
exit: { y: -40, opacity: 0 }, |
|
|
|
|
|
transition: { duration: .2, ease: 'easeOut' } })} |
|
|
|
|
|
|
|
|
: { initial: { x: 40, y: -40, opacity: 0 }, |
|
|
|
|
|
animate: { x: 0, y: 0, opacity: 1 }, |
|
|
|
|
|
exit: { x: 40, y: -40, opacity: 0 } })} |
|
|
className="z-10 h-full flex items-center px-3 font-bold w-24"> |
|
|
className="z-10 h-full flex items-center px-3 font-bold w-24"> |
|
|
{item.name} |
|
|
|
|
|
</motion.h2> |
|
|
|
|
|
|
|
|
<h2>{item.name}</h2> |
|
|
|
|
|
</motion.div> |
|
|
{item.subMenu |
|
|
{item.subMenu |
|
|
.filter (subItem => subItem.visible ?? true) |
|
|
.filter (subItem => subItem.visible ?? true) |
|
|
.map ((subItem, j) => ( |
|
|
.map ((subItem, j) => ( |
|
|
@@ -269,21 +268,24 @@ export default (({ user }: Props) => { |
|
|
? ( |
|
|
? ( |
|
|
<motion.div |
|
|
<motion.div |
|
|
key={`c-${ i }-${ j }`} |
|
|
key={`c-${ i }-${ j }`} |
|
|
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name |
|
|
|
|
|
|
|
|
transition={{ duration: .2, ease: 'easeOut' }} |
|
|
|
|
|
{...((visibleMenu[activeIdx]?.name |
|
|
=== item.name) |
|
|
=== item.name) |
|
|
? { layoutId: `submenu-${ item.name }-${ j }` } |
|
|
? { layoutId: `submenu-${ item.name }-${ j }` } |
|
|
: { initial: { y: -40, opacity: 0 }, |
|
|
: { initial: { y: -40, opacity: 0 }, |
|
|
animate: { y: 0, opacity: 1 }, |
|
|
animate: { y: 0, opacity: 1 }, |
|
|
exit: { y: -40, opacity: 0 }, |
|
|
|
|
|
transition: { duration: .2, ease: 'easeOut' } })}> |
|
|
|
|
|
|
|
|
exit: { y: -40, opacity: 0 } })}> |
|
|
{subItem.component} |
|
|
{subItem.component} |
|
|
</motion.div>) |
|
|
</motion.div>) |
|
|
: ( |
|
|
: ( |
|
|
<motion.div |
|
|
<motion.div |
|
|
key={`l-${ i }-${ j }`} |
|
|
key={`l-${ i }-${ j }`} |
|
|
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name |
|
|
|
|
|
|
|
|
{...((visibleMenu[activeIdx]?.name |
|
|
=== item.name) |
|
|
=== item.name) |
|
|
&& { layoutId: `submenu-${ item.name }-${ j }` })}> |
|
|
|
|
|
|
|
|
? { layoutId: `submenu-${ item.name }-${ j }` } |
|
|
|
|
|
: { initial: { y: -40, opacity: 0 }, |
|
|
|
|
|
animate: { y: 0, opacity: 1 }, |
|
|
|
|
|
exit: { y: -40, opacity: 0 } })}> |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to={subItem.to} |
|
|
to={subItem.to} |
|
|
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined} |
|
|
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined} |
|
|
@@ -293,7 +295,7 @@ export default (({ user }: Props) => { |
|
|
</motion.div>)))} |
|
|
</motion.div>)))} |
|
|
</div> |
|
|
</div> |
|
|
</div>))) |
|
|
</div>))) |
|
|
: ((menu.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? []).length > 0 |
|
|
|
|
|
|
|
|
: ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0 |
|
|
&& ( |
|
|
&& ( |
|
|
<div className="relative h-[40px]"> |
|
|
<div className="relative h-[40px]"> |
|
|
<AnimatePresence initial={false} custom={dir}> |
|
|
<AnimatePresence initial={false} custom={dir}> |
|
|
@@ -308,24 +310,20 @@ export default (({ user }: Props) => { |
|
|
animate="centre" |
|
|
animate="centre" |
|
|
exit="exit" |
|
|
exit="exit" |
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
{(menu.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? []) |
|
|
|
|
|
|
|
|
{(visibleMenu[activeIdx]?.subMenu ?? []) |
|
|
.filter (item => item.visible ?? true) |
|
|
.filter (item => item.visible ?? true) |
|
|
.map ((item, i) => ( |
|
|
.map ((item, i) => ( |
|
|
'component' in item |
|
|
'component' in item |
|
|
? ( |
|
|
? ( |
|
|
<motion.div |
|
|
<motion.div |
|
|
key={`c-${ i }`} |
|
|
key={`c-${ i }`} |
|
|
layoutId={`submenu-${ |
|
|
|
|
|
menu.filter (x => x.visible ?? true)[activeIdx].name }-${ |
|
|
|
|
|
i }`}> |
|
|
|
|
|
|
|
|
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}> |
|
|
{item.component} |
|
|
{item.component} |
|
|
</motion.div>) |
|
|
</motion.div>) |
|
|
: ( |
|
|
: ( |
|
|
<motion.div |
|
|
<motion.div |
|
|
key={`l-${ i }`} |
|
|
key={`l-${ i }`} |
|
|
layoutId={`submenu-${ |
|
|
|
|
|
menu.filter (x => x.visible ?? true)[activeIdx].name }-${ |
|
|
|
|
|
i }`}> |
|
|
|
|
|
|
|
|
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}> |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to={item.to} |
|
|
to={item.to} |
|
|
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined} |
|
|
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined} |
|
|
@@ -354,8 +352,7 @@ export default (({ user }: Props) => { |
|
|
exit="closed" |
|
|
exit="closed" |
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
transition={{ duration: .2, ease: 'easeOut' }}> |
|
|
<Separator/> |
|
|
<Separator/> |
|
|
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) |
|
|
|
|
|
.map ((item, i) => ( |
|
|
|
|
|
|
|
|
{visibleMenu.map ((item, i) => ( |
|
|
<Fragment key={i}> |
|
|
<Fragment key={i}> |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to={i === openItemIdx ? item.to : '#'} |
|
|
to={i === openItemIdx ? item.to : '#'} |
|
|
@@ -411,7 +408,7 @@ export default (({ user }: Props) => { |
|
|
<PrefetchLink |
|
|
<PrefetchLink |
|
|
to="/more" |
|
|
to="/more" |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
ref={(el: (HTMLAnchorElement | null)) => { |
|
|
itemsRef.current[menu.length] = el |
|
|
|
|
|
|
|
|
itemsRef.current[visibleMenu.length] = el |
|
|
}} |
|
|
}} |
|
|
className={cn ('w-full min-h-[40px] flex items-center pl-8', |
|
|
className={cn ('w-full min-h-[40px] flex items-center pl-8', |
|
|
((openItemIdx < 0) |
|
|
((openItemIdx < 0) |
|
|
|