|
- 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 itemRefs = useRef<(HTMLAnchorElement | null)[]> ([])
- const navRef = useRef<HTMLDivElement | null> (null)
-
- const measure = () => {
- const nav = navRef.current
- const el = itemRefs.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)
- const [subDir, setSubDir] = useState<(-1) | 1> (1)
- const [wikiId, setWikiId] = useState<number | null> (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: <Separator/>, 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<number> (activeIdx)
-
- 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 (() => {
- const prev = prevActiveIdxRef.current
- if (prev !== activeIdx)
- {
- setSubDir (activeIdx > prev ? 1 : -1)
- prevActiveIdxRef.current = activeIdx
- }
- }, [activeIdx])
-
- 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 (
- <>
- <nav className="px-3 flex justify-between items-center w-full min-h-[48px]
- bg-yellow-200 dark:bg-red-975 md:bg-yellow-50">
- <div className="flex items-center gap-2 h-full">
- <Link to="/"
- className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400
- dark:text-pink-300 dark:hover:text-pink-100">
- ぼざクリ タグ広場
- </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 rounded-md',
- 'bg-yellow-200 dark:bg-red-950',
- 'transition-[transform,width,opacity] 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 => {
- itemRefs.current[i] = el
- }}
- className={cn ('relative z-10 flex h-full items-center px-5',
- ((i === openItemIdx)
- ? 'font-bold'
- : 'opacity-90 hover:opacity-100'))}>
- {item.name}
- </Link>))}
- </div>
- </div>
-
- <TopNavUser user={user}/>
-
- <a href="#"
- className="md:hidden ml-auto pr-4
- text-pink-600 hover:text-pink-400
- dark:text-pink-300 dark:hover:text-pink-100"
- onClick={ev => {
- ev.preventDefault ()
- setMenuOpen (!(menuOpen))
- }}>
- {menuOpen ? '×' : 'Menu'}
- </a>
- </nav>
-
- <div className="hidden md:flex bg-yellow-200 dark:bg-red-950
- items-center w-full min-h-[40px] px-3 overflow-hidden">
- <AnimatePresence mode="wait" initial={false}>
- <motion.div
- key={activeIdx}
- className="flex items-center"
- initial={{ y: subDir * 24, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- exit={{ y: subDir * 24, opacity: 0 }}
- transition={{ duration: .1, 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}>
- {menuOpen && (
- <motion.div
- key="spmenu"
- className={cn ('flex flex-col md:hidden',
- 'bg-yellow-200 dark:bg-red-975 items-start')}
- variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
- height: 0 },
- open: { clipPath: 'inset(0 0 0% 0)',
- height: 'auto' } }}
- initial="closed"
- animate="open"
- exit="closed"
- transition={{ duration: .2, ease: 'easeOut' }}>
- <Separator/>
- {menu.map ((item, i) => (
- <Fragment key={i}>
- <Link to={i === openItemIdx ? item.to : '#'}
- className={cn ('w-full min-h-[40px] flex items-center pl-8',
- ((i === openItemIdx)
- && 'font-bold bg-yellow-50 dark:bg-red-950'))}
- onClick={ev => {
- if (i !== openItemIdx)
- {
- ev.preventDefault ()
- setOpenItemIdx (i)
- }
- }}>
- {item.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/>
- </motion.div>)}
- </AnimatePresence>
- </>)
- }) satisfies FC<Props>
|