ぼざクリ タグ広場 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.
 
 
 
 
 
 

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