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

233 lines
8.7 KiB

  1. import axios from 'axios'
  2. import toCamel from 'camelcase-keys'
  3. import React, { useState, useEffect } from 'react'
  4. import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
  5. import SettingsDialogue from '@/components/SettingsDialogue'
  6. import { Button } from '@/components/ui/button'
  7. import { API_BASE_URL } from '@/config'
  8. import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
  9. import { cn } from '@/lib/utils'
  10. import type { Tag, User, WikiPage } from '@/types'
  11. type Props = { user: User
  12. setUser: (user: User) => void }
  13. const enum Menu { None,
  14. Post,
  15. User,
  16. Tag,
  17. Wiki }
  18. const TopNav: React.FC = ({ user, setUser }: Props) => {
  19. const location = useLocation ()
  20. const navigate = useNavigate ()
  21. const [settingsVsbl, setSettingsVsbl] = useState (false)
  22. const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None)
  23. const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
  24. const [wikiSearch, setWikiSearch] = useState ('')
  25. const [activeIndex, setActiveIndex] = useState (-1)
  26. const [suggestions, setSuggestions] = useState<WikiPage[]> ([])
  27. const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
  28. const [tagSearch, setTagSearch] = useState ('')
  29. const [userSearch, setUserSearch] = useState ('')
  30. const [postCount, setPostCount] = useState<number | null> (null)
  31. const MyLink = ({ to, title, menu, base }: { to: string
  32. title: string
  33. menu?: Menu
  34. base?: string }) => (
  35. <Link to={to} className={cn ('hover:text-orange-500 h-full flex items-center',
  36. (location.pathname.startsWith (base ?? to)
  37. ? 'bg-gray-700 px-4 font-bold'
  38. : 'px-2'))}>
  39. {title}
  40. </Link>)
  41. const whenTagSearchChanged = ev => {
  42. // TODO: 実装
  43. setTagSearch (ev.target.value)
  44. const q: string = ev.target.value.split (' ').at (-1)
  45. if (!(q))
  46. {
  47. setSuggestions ([])
  48. return
  49. }
  50. }
  51. const whenWikiSearchChanged = ev => {
  52. // TODO: 実装
  53. setWikiSearch (ev.target.value)
  54. const q: string = ev.target.value.split (' ').at (-1)
  55. if (!(q))
  56. {
  57. setSuggestions ([])
  58. return
  59. }
  60. }
  61. const whenUserSearchChanged = ev => {
  62. // TODO: 実装
  63. setUserSearch (ev.target.value)
  64. const q: string = ev.target.value.split (' ').at (-1)
  65. if (!(q))
  66. {
  67. setSuggestions ([])
  68. return
  69. }
  70. }
  71. const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  72. if (e.key === 'Enter' && wikiSearch.length && (!(suggestionsVsbl) || activeIndex < 0))
  73. {
  74. navigate (`/wiki/${ encodeURIComponent (wikiSearch) }`)
  75. setSuggestionsVsbl (false)
  76. }
  77. }
  78. const handleTagSelect = (tag: Tag) => {
  79. }
  80. useEffect (() => {
  81. const unsubscribe = WikiIdBus.subscribe (setWikiId)
  82. return () => unsubscribe ()
  83. }, [])
  84. useEffect (() => {
  85. if (location.pathname.startsWith ('/posts'))
  86. setSelectedMenu (Menu.Post)
  87. else if (location.pathname.startsWith ('/users'))
  88. setSelectedMenu (Menu.User)
  89. else if (location.pathname.startsWith ('/tags'))
  90. setSelectedMenu (Menu.Tag)
  91. else if (location.pathname.startsWith ('/wiki'))
  92. setSelectedMenu (Menu.Wiki)
  93. else
  94. setSelectedMenu (Menu.None)
  95. }, [location])
  96. useEffect (() => {
  97. if (!(wikiId))
  98. return
  99. void ((async () => {
  100. try
  101. {
  102. const { data: pageData } = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`)
  103. const wikiPage: WikiPage = toCamel (pageData, { deep: true })
  104. const { data: tagData } = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`)
  105. const tag: Tag = toCamel (tagData, { deep: true })
  106. setPostCount (tag.postCount)
  107. }
  108. catch
  109. {
  110. setPostCount (0)
  111. }
  112. }) ())
  113. }, [wikiId])
  114. return (
  115. <>
  116. <nav className="bg-gray-800 text-white px-3 flex justify-between items-center w-full min-h-[48px]">
  117. <div className="flex items-center gap-2 h-full">
  118. <Link to="/posts" className="mx-4 text-xl font-bold text-orange-500">ぼざクリ タグ広場</Link>
  119. <MyLink to="/posts" title="広場" />
  120. <MyLink to="/tags" title="タグ" />
  121. <MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
  122. <MyLink to="/users" title="ニジラー" />
  123. </div>
  124. <div className="ml-auto pr-4">
  125. <Button onClick={() => setSettingsVsbl (true)}>{user?.name || '名もなきニジラー'}</Button>
  126. <SettingsDialogue visible={settingsVsbl}
  127. onVisibleChange={setSettingsVsbl}
  128. user={user}
  129. setUser={setUser} />
  130. </div>
  131. </nav>
  132. {(() => {
  133. const className = 'bg-gray-700 text-white px-3 flex items-center w-full min-h-[40px]'
  134. const subClass = 'hover:text-orange-500 h-full flex items-center px-3'
  135. const inputBox = 'flex items-center px-3 mx-2'
  136. const Separator = () => <span className="flex items-center px-2">|</span>
  137. switch (selectedMenu)
  138. {
  139. case Menu.Post:
  140. return (
  141. <div className={className}>
  142. <Link to="/posts" className={subClass}>一覧</Link>
  143. <Link to="/posts/new" className={subClass}>投稿追加</Link>
  144. <Link to="/wiki/ヘルプ:広場" className={subClass}>ヘルプ</Link>
  145. </div>)
  146. case Menu.Tag:
  147. return (
  148. <div className={className}>
  149. <input type="text"
  150. className={inputBox}
  151. placeholder="タグ検索"
  152. value={tagSearch}
  153. onChange={whenTagSearchChanged}
  154. onFocus={() => setSuggestionsVsbl (true)}
  155. onBlur={() => setSuggestionsVsbl (false)}
  156. onKeyDown={handleKeyDown} />
  157. <Link to="/tags" className={subClass}>タグ</Link>
  158. <Link to="/tags/aliases" className={subClass}>別名タグ</Link>
  159. <Link to="/tags/implications" className={subClass}>上位タグ</Link>
  160. <Link to="/wiki/ヘルプ:タグのつけ方" className={subClass}>タグのつけ方</Link>
  161. <Link to="/wiki/ヘルプ:タグ" className={subClass}>ヘルプ</Link>
  162. </div>)
  163. case Menu.Wiki:
  164. return (
  165. <div className={className}>
  166. <input type="text"
  167. className={inputBox}
  168. placeholder="Wiki 検索"
  169. value={wikiSearch}
  170. onChange={whenWikiSearchChanged}
  171. onFocus={() => setSuggestionsVsbl (true)}
  172. onBlur={() => setSuggestionsVsbl (false)}
  173. onKeyDown={handleKeyDown} />
  174. <Link to="/wiki" className={subClass}>検索</Link>
  175. <Link to="/wiki/new" className={subClass}>新規</Link>
  176. <Link to="/wiki/changes" className={subClass}>全体履歴</Link>
  177. <Link to="/wiki/ヘルプ:Wiki" className={subClass}>ヘルプ</Link>
  178. {(/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) &&
  179. <>
  180. <Separator />
  181. <Link to={`/posts?tags=${ location.pathname.split ('/')[2] }`} className={subClass}>広場 ({postCount || 0})</Link>
  182. <Link to={`/wiki/changes?id=${ wikiId }`} className={subClass}>履歴</Link>
  183. <Link to={`/wiki/${ wikiId || location.pathname.split ('/')[2] }/edit`} className={subClass}>編輯</Link>
  184. </>}
  185. </div>)
  186. case Menu.User:
  187. return (
  188. <div className={className}>
  189. <input type="text"
  190. className={inputBox}
  191. placeholder="ニジラー検索"
  192. value={userSearch}
  193. onChange={whenUserSearchChanged}
  194. onFocus={() => setSuggestionsVsbl (true)}
  195. onBlur={() => setSuggestionsVsbl (false)}
  196. onKeyDown={handleKeyDown} />
  197. <Link to="/users" className={subClass}>一覧</Link>
  198. {user && <Link to={`/users/${ user.id }`} className={subClass}>お前</Link>}
  199. </div>)
  200. }
  201. }) ()}
  202. </>)
  203. }
  204. export default TopNav