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