import axios from 'axios' import toCamel from 'camelcase-keys' import { AnimatePresence, motion } from 'framer-motion' import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' import { Link, useLocation } from 'react-router-dom' import Separator from '@/components/MenuSeparator' import TopNavUser from '@/components/TopNavUser' import { API_BASE_URL } from '@/config' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { cn } from '@/lib/utils' import type { FC } from 'react' import type { Menu, Tag, User, WikiPage } from '@/types' 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 (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 (null) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) const wikiTitle = location.pathname.split ('/')[2] const menu: Menu = [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, { name: '投稿追加', to: '/posts/new' }, { name: '耕作履歴', to: '/posts/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'タグ', to: '/tags', subMenu: [ { name: 'タグ一覧', to: '/tags', visible: false }, { name: '別名タグ', to: '/tags/aliases', visible: false }, { name: '上位タグ', to: '/tags/implications', visible: false }, { name: 'ニコニコ連携', to: '/tags/nico' }, { 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', subMenu: [ { name: '一覧', to: '/users', visible: false }, { 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 (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 () }, []) useEffect (() => { setMenuOpen (false) setOpenItemIdx (menu.findIndex (item => ( location.pathname.startsWith (item.base || item.to)))) }, [location]) useEffect (() => { if (!(wikiId)) return const fetchPostCount = async () => { try { const pageRes = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`) const wikiPage = toCamel (pageRes.data as any, { deep: true }) as WikiPage const tagRes = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`) const tag = toCamel (tagRes.data as any, { deep: true }) as Tag setPostCount (tag.postCount) } catch { setPostCount (0) } } fetchPostCount () }, [wikiId]) return ( <>
({ 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 ? {item.component} : ( {item.name} )))}
{menuOpen && ( {menu.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} )))} )} ))} )} ) }) satisfies FC