ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

173 lines
5.6 KiB

  1. import axios from 'axios'
  2. import toCamel from 'camelcase-keys'
  3. import { Fragment, useState, useEffect } from 'react'
  4. import { Link, useLocation } from 'react-router-dom'
  5. import Separator from '@/components/MenuSeparator'
  6. import TopNavUser from '@/components/TopNavUser'
  7. import { API_BASE_URL } from '@/config'
  8. import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
  9. import { cn } from '@/lib/utils'
  10. import type { FC } from 'react'
  11. import type { Menu, Tag, User, WikiPage } from '@/types'
  12. type Props = { user: User | null }
  13. export default (({ user }: Props) => {
  14. const location = useLocation ()
  15. const [menuOpen, setMenuOpen] = useState (false)
  16. const [openItemIdx, setOpenItemIdx] = useState (-1)
  17. const [postCount, setPostCount] = useState<number | null> (null)
  18. const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
  19. const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
  20. const wikiTitle = location.pathname.split ('/')[2]
  21. const menu: Menu = [
  22. { name: '広場', to: '/posts', subMenu: [
  23. { name: '一覧', to: '/posts' },
  24. { name: '投稿追加', to: '/posts/new' },
  25. { name: '耕作履歴', to: '/posts/changes' },
  26. { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
  27. { name: 'タグ', to: '/tags', subMenu: [
  28. { name: 'タグ一覧', to: '/tags', visible: false },
  29. { name: '別名タグ', to: '/tags/aliases', visible: false },
  30. { name: '上位タグ', to: '/tags/implications', visible: false },
  31. { name: 'ニコニコ連携', to: '/tags/nico' },
  32. { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
  33. { name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [
  34. { name: '検索', to: '/wiki' },
  35. { name: '新規', to: '/wiki/new' },
  36. { name: '全体履歴', to: '/wiki/changes' },
  37. { name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
  38. { component: <Separator/>, visible: wikiPageFlg },
  39. { name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
  40. visible: wikiPageFlg },
  41. { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
  42. { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] },
  43. { name: 'ユーザ', to: '/users', subMenu: [
  44. { name: '一覧', to: '/users', visible: false },
  45. { name: 'お前', to: `/users/${ user?.id }`, visible: false },
  46. { name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
  47. useEffect (() => {
  48. const unsubscribe = WikiIdBus.subscribe (setWikiId)
  49. return () => unsubscribe ()
  50. }, [])
  51. useEffect (() => {
  52. setMenuOpen (false)
  53. setOpenItemIdx (menu.findIndex (item => (
  54. location.pathname.startsWith (item.base || item.to))))
  55. }, [location])
  56. useEffect (() => {
  57. if (!(wikiId))
  58. return
  59. const fetchPostCount = async () => {
  60. try
  61. {
  62. const pageRes = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`)
  63. const wikiPage = toCamel (pageRes.data as any, { deep: true }) as WikiPage
  64. const tagRes = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`)
  65. const tag = toCamel (tagRes.data as any, { deep: true }) as Tag
  66. setPostCount (tag.postCount)
  67. }
  68. catch
  69. {
  70. setPostCount (0)
  71. }
  72. }
  73. fetchPostCount ()
  74. }, [wikiId])
  75. return (
  76. <>
  77. <nav className="px-3 flex justify-between items-center w-full min-h-[48px]
  78. bg-yellow-200 dark:bg-red-975 md:bg-yellow-50">
  79. <div className="flex items-center gap-2 h-full">
  80. <Link to="/"
  81. className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400
  82. dark:text-pink-300 dark:hover:text-pink-100">
  83. ぼざクリ タグ広場
  84. </Link>
  85. {menu.map ((item, i) => (
  86. <Link key={i}
  87. to={item.to}
  88. className={cn ('hidden md:flex h-full items-center',
  89. (location.pathname.startsWith (item.base || item.to)
  90. ? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
  91. : 'px-2'))}>
  92. {item.name}
  93. </Link>
  94. ))}
  95. </div>
  96. <TopNavUser user={user}/>
  97. <a href="#"
  98. className="md:hidden ml-auto pr-4
  99. text-pink-600 hover:text-pink-400
  100. dark:text-pink-300 dark:hover:text-pink-100"
  101. onClick={ev => {
  102. ev.preventDefault ()
  103. setMenuOpen (!(menuOpen))
  104. }}>
  105. {menuOpen ? '×' : 'Menu'}
  106. </a>
  107. </nav>
  108. <div className="hidden md:flex bg-yellow-200 dark:bg-red-950
  109. items-center w-full min-h-[40px] px-3">
  110. {menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
  111. .filter (item => item.visible ?? true)
  112. .map ((item, i) => 'component' in item ? item.component : (
  113. <Link key={i}
  114. to={item.to}
  115. className="h-full flex items-center px-3">
  116. {item.name}
  117. </Link>))}
  118. </div>
  119. <div className={cn (menuOpen ? 'flex flex-col md:hidden' : 'hidden',
  120. 'bg-yellow-200 dark:bg-red-975 items-start')}>
  121. <Separator/>
  122. {menu.map ((item, i) => (
  123. <Fragment key={i}>
  124. <Link to={i === openItemIdx ? item.to : '#'}
  125. className={cn ('w-full min-h-[40px] flex items-center pl-8',
  126. ((i === openItemIdx)
  127. && 'font-bold bg-yellow-50 dark:bg-red-950'))}
  128. onClick={ev => {
  129. if (i !== openItemIdx)
  130. {
  131. ev.preventDefault ()
  132. setOpenItemIdx (i)
  133. }
  134. }}>
  135. {item.name}
  136. </Link>
  137. {i === openItemIdx && (
  138. item.subMenu
  139. .filter (subItem => subItem.visible ?? true)
  140. .map ((subItem, j) => 'component' in subItem ? subItem.component : (
  141. <Link key={j}
  142. to={subItem.to}
  143. className="w-full min-h-[36px] flex items-center pl-12
  144. bg-yellow-50 dark:bg-red-950">
  145. {subItem.name}
  146. </Link>)))}
  147. </Fragment>))}
  148. <TopNavUser user={user} sp/>
  149. <Separator/>
  150. </div>
  151. </>)
  152. }) satisfies FC<Props>