import { useQuery } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useLocation } from 'react-router-dom' import Separator from '@/components/MenuSeparator' import PrefetchLink from '@/components/PrefetchLink' import TopNavUser from '@/components/TopNavUser' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { tagsKeys, wikiKeys } from '@/lib/queryKeys' import { fetchTag, fetchTagByName } from '@/lib/tags' import { cn } from '@/lib/utils' import { fetchWikiPage } from '@/lib/wiki' import type { FC, MouseEvent } from 'react' import type { Menu, MenuVisibleItem, Tag, User } from '@/types' type Props = { user: User | null } 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 wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (pathName) && wikiId) const wikiTitle = pathName.split ('/')[2] ?? '' const tagFlg = /^\/tags\/\d+/.test (pathName) return [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, { name: '検索', to: '/posts/search' }, { name: '追加', to: '/posts/new' }, { name: '履歴', to: '/posts/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'タグ', to: '/tags', subMenu: [ { name: 'マスタ', to: '/tags' }, { name: 'ニコニコ連携', to: '/tags/nico' }, { name: '履歴', to: '/tags/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }, { component: , visible: tagFlg }, { name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`, visible: tagFlg }, { name: '履歴', to: `/tags/changes?id=${ tag?.id }`, visible: tagFlg && tag?.category !== 'nico' }] }, { name: '素材', to: '/materials', visible: false, subMenu: [ { name: '一覧', to: '/materials' }, { name: '検索', to: '/materials/search', visible: false }, { name: '追加', to: '/materials/new' }, { name: '履歴', to: '/materials/changes', visible: false }, { name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] }, { name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [ { name: <>第 1 会場, to: '/theatres/1' }, { name: 'CyTube', to: '//cytube.mm428.net/r/deernijika' }, { name: <>ニジカ放送局第 1 チャンネル, to: '//www.youtube.com/watch?v=DCU3hL4Uu6A' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:上映会' }] }, { name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [ { name: '検索', to: '/wiki' }, { name: '新規', to: '/wiki/new' }, { name: '全体履歴', to: '/wiki/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' }, { component: , visible: wikiPageFlg }, { name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`, visible: wikiPageFlg }, { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg }, { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] }, { name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [ { name: '一覧', to: '/users', visible: false }, { name: 'お前', to: `/users/${ user?.id }`, visible: false }, { name: '設定', to: '/users/settings', visible: Boolean (user) }] }, { name: '法規', visible: false, subMenu: [ { name: '利用規約', to: '/tos' }] }] } export default (({ user }: Props) => { const location = useLocation () const dirRef = useRef<(-1) | 1> (1) const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) const navRef = useRef (null) const measure = (idx: number) => { const nav = navRef.current const el = itemsRef.current[idx < 0 ? visibleMenu.length : idx] if (!(nav) || !(el)) { setHL ({ left: 0, width: 0, visible: true }) 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 [moreVsbl, setMoreVsbl] = useState (false) const [openItemIdx, setOpenItemIdx] = useState (-1) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) const wikiIdStr = String (wikiId ?? '') const { data: wikiPage } = useQuery ({ enabled: Boolean (wikiIdStr), queryKey: wikiKeys.show (wikiIdStr, { }), queryFn: () => fetchWikiPage (wikiIdStr, { }) }) const tagFlg = /^\/tags\/\d+/.test (location.pathname) const effectiveTitle = (tagFlg ? location.pathname.split ('/')[2] : wikiPage?.title) ?? '' const { data: tag } = useQuery ({ enabled: Boolean (effectiveTitle), queryKey: tagsKeys.show (effectiveTitle), queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle) }) 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 (activeIdx) if (activeIdx !== prevActiveIdxRef.current) { dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 prevActiveIdxRef.current = activeIdx } const dir = dirRef.current useLayoutEffect (() => { const raf = requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) const onResize = () => requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) addEventListener ('resize', onResize) return () => { cancelAnimationFrame (raf) removeEventListener ('resize', onResize) } }) useEffect (() => { const unsubscribe = WikiIdBus.subscribe (setWikiId) return () => unsubscribe () }, [activeIdx]) useEffect (() => { setMenuOpen (false) setOpenItemIdx (activeIdx) }, [location]) return ( <> { if (moreVsbl) setMoreVsbl (false) }} transition={{ layout: { duration: .2, ease: 'easeOut' } }} onAnimationComplete={() => { measure (moreVsbl ? -1 : activeIdx) }}> {moreVsbl ? ( menu.map ((item, i) => (

{item.name}

{item.subMenu .filter (subItem => subItem.visible ?? true) .map ((subItem, j) => ( 'component' in subItem ? ( {subItem.component} ) : ( setMoreVsbl (false)} className="h-full flex items-center px-3"> {subItem.name} )))}
))) : ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0 && (
({ 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' }}> {(visibleMenu[activeIdx]?.subMenu ?? []) .filter (item => item.visible ?? true) .map ((item, i) => ( 'component' in item ? ( {item.component} ) : ( {item.name} )))}
))}
{menuOpen && ( {visibleMenu.map ((item, i) => ( ) => { if (i !== openItemIdx) { ev.preventDefault () setOpenItemIdx (i) } }}> {item.name} {i === openItemIdx && ( {item.subMenu .filter (subItem => subItem.visible ?? true) .map ((subItem, j) => ( 'component' in subItem ? ( {subItem.component} ) : ( {subItem.name} )))} )} ))} { itemsRef.current[visibleMenu.length] = el }} className={cn ('w-full min-h-[40px] flex items-center pl-8', ((openItemIdx < 0) && 'font-bold bg-yellow-50 dark:bg-red-950'))}> その他 » )} ) }) satisfies FC