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

136 lines
3.7 KiB

  1. import axios from 'axios'
  2. import toCamel from 'camelcase-keys'
  3. import { useEffect, useState } from 'react'
  4. import { Helmet } from 'react-helmet-async'
  5. import { useParams } from 'react-router-dom'
  6. import TagDetailSidebar from '@/components/TagDetailSidebar'
  7. import NicoViewer from '@/components/NicoViewer'
  8. import PostEditForm from '@/components/PostEditForm'
  9. import TabGroup, { Tab } from '@/components/common/TabGroup'
  10. import MainArea from '@/components/layout/MainArea'
  11. import { Button } from '@/components/ui/button'
  12. import { toast } from '@/components/ui/use-toast'
  13. import { API_BASE_URL, SITE_TITLE } from '@/config'
  14. import { cn } from '@/lib/utils'
  15. import NotFound from '@/pages/NotFound'
  16. import ServiceUnavailable from '@/pages/ServiceUnavailable'
  17. import type { Post, User } from '@/types'
  18. type Props = { user: User | null }
  19. export default ({ user }: Props) => {
  20. const { id } = useParams ()
  21. const [editing, setEditing] = useState (true)
  22. const [post, setPost] = useState<Post | null> (null)
  23. const [status, setStatus] = useState (200)
  24. const changeViewedFlg = async () => {
  25. const url = `${ API_BASE_URL }/posts/${ id }/viewed`
  26. const opt = { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }
  27. try
  28. {
  29. if (post!.viewed)
  30. await axios.delete (url, opt)
  31. else
  32. await axios.post (url, { }, opt)
  33. // 通信に成功したら “閲覧済” をトグル
  34. setPost (post => ({ ...post!, viewed: !(post!.viewed) }))
  35. }
  36. catch
  37. {
  38. toast ({ title: '失敗……', description: '通信に失敗しました……' })
  39. }
  40. }
  41. useEffect (() => {
  42. if (!(id))
  43. return
  44. void (async () => {
  45. try
  46. {
  47. const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
  48. 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
  49. setPost (toCamel (res.data as any, { deep: true }) as Post)
  50. }
  51. catch (err)
  52. {
  53. if (axios.isAxiosError (err))
  54. setStatus (err.status ?? 200)
  55. }
  56. }) ()
  57. }, [id])
  58. useEffect (() => {
  59. setEditing (false)
  60. }, [post])
  61. useEffect (() => {
  62. if (!(editing))
  63. setEditing (true)
  64. }, [editing])
  65. switch (status)
  66. {
  67. case 404:
  68. return <NotFound />
  69. case 503:
  70. return <ServiceUnavailable />
  71. }
  72. const url = post ? new URL (post.url) : null
  73. const nicoFlg = url?.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp'
  74. const match = nicoFlg ? url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/) : null
  75. const videoId = match?.[0] ?? ''
  76. const viewedClass = (post?.viewed
  77. ? 'bg-blue-600 hover:bg-blue-700'
  78. : 'bg-gray-500 hover:bg-gray-600')
  79. return (
  80. <div className="md:flex md:flex-1">
  81. <Helmet>
  82. {(post?.thumbnail || post?.thumbnailBase) && (
  83. <meta name="thumbnail" content={post.thumbnail || post.thumbnailBase} />)}
  84. {post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
  85. </Helmet>
  86. <div className="hidden md:block">
  87. <TagDetailSidebar post={post} />
  88. </div>
  89. <MainArea>
  90. {post
  91. ? (
  92. <>
  93. {nicoFlg
  94. ? (
  95. <NicoViewer id={videoId}
  96. width={640}
  97. height={360} />)
  98. : <img src={post.thumbnail} alt={post.url} className="mb-4 w-full" />}
  99. <Button onClick={changeViewedFlg}
  100. className={cn ('text-white', viewedClass)}>
  101. {post.viewed ? '閲覧済' : '未閲覧'}
  102. </Button>
  103. <TabGroup>
  104. {(['admin', 'member'].some (r => user?.role === r) && editing) && (
  105. <Tab name="編輯">
  106. <PostEditForm post={post}
  107. onSave={newPost => {
  108. setPost (newPost)
  109. toast ({ description: '更新しました.' })
  110. }} />
  111. </Tab>)}
  112. </TabGroup>
  113. </>)
  114. : 'Loading...'}
  115. </MainArea>
  116. <div className="md:hidden">
  117. <TagDetailSidebar post={post} />
  118. </div>
  119. </div>)
  120. }