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

132 lines
3.8 KiB

  1. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
  2. import { useEffect, useState } from 'react'
  3. import { Helmet } from 'react-helmet-async'
  4. import { useParams } from 'react-router-dom'
  5. import PostList from '@/components/PostList'
  6. import TagDetailSidebar from '@/components/TagDetailSidebar'
  7. import PostEditForm from '@/components/PostEditForm'
  8. import PostEmbed from '@/components/PostEmbed'
  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 { SITE_TITLE } from '@/config'
  14. import { fetchPost, toggleViewedFlg } from '@/lib/posts'
  15. import { cn } from '@/lib/utils'
  16. import NotFound from '@/pages/NotFound'
  17. import ServiceUnavailable from '@/pages/ServiceUnavailable'
  18. import type { FC } from 'react'
  19. import type { User } from '@/types'
  20. type Props = { user: User | null }
  21. export default (({ user }: Props) => {
  22. const { id } = useParams ()
  23. const { data: post, isError: errorFlg, error } = useQuery ({
  24. enabled: Boolean (id),
  25. queryKey: ['posts', String (id)],
  26. queryFn: () => fetchPost (String (id)) })
  27. const qc = useQueryClient ()
  28. const [status, setStatus] = useState (200)
  29. const changeViewedFlg = useMutation ({
  30. mutationFn: async () => {
  31. const next = !(post!.viewed)
  32. await toggleViewedFlg (id!, next)
  33. return next
  34. },
  35. onMutate: async () => {
  36. await qc.cancelQueries ({ queryKey: ['posts', String (id)] })
  37. const prev = qc.getQueryData<any> (['posts', String (id)])
  38. qc.setQueryData (['posts', String (id)],
  39. (cur: any) => cur ? { ...cur, viewed: !(cur.viewed) } : cur)
  40. return { prev }
  41. },
  42. onError: (...[, , ctx]) => {
  43. if (ctx?.prev)
  44. qc.setQueryData (['posts', String (id)], ctx.prev)
  45. toast ({ title: '失敗……', description: '通信に失敗しました……' })
  46. },
  47. onSuccess: () => {
  48. qc.invalidateQueries ({ queryKey: ['posts', 'index'] })
  49. qc.invalidateQueries ({ queryKey: ['related', String (id)] })
  50. } })
  51. useEffect (() => {
  52. if (!(errorFlg))
  53. return
  54. const code = (error as any)?.response.status ?? (error as any)?.status
  55. if (code)
  56. setStatus (code)
  57. }, [errorFlg, error])
  58. switch (status)
  59. {
  60. case 404:
  61. return <NotFound/>
  62. case 503:
  63. return <ServiceUnavailable/>
  64. }
  65. const viewedClass = (post?.viewed
  66. ? 'bg-blue-600 hover:bg-blue-700'
  67. : 'bg-gray-500 hover:bg-gray-600')
  68. return (
  69. <div className="md:flex md:flex-1">
  70. <Helmet>
  71. {(post?.thumbnail || post?.thumbnailBase) && (
  72. <meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
  73. {post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
  74. </Helmet>
  75. <div className="hidden md:block">
  76. <TagDetailSidebar post={post ?? null}/>
  77. </div>
  78. <MainArea>
  79. {post
  80. ? (
  81. <>
  82. <PostEmbed post={post}/>
  83. <Button onClick={() => changeViewedFlg.mutate ()}
  84. disabled={changeViewedFlg.isPending}
  85. className={cn ('text-white', viewedClass)}>
  86. {post.viewed ? '閲覧済' : '未閲覧'}
  87. </Button>
  88. <TabGroup>
  89. <Tab name="関聯">
  90. {post.related.length > 0
  91. ? <PostList posts={post.related}/>
  92. : 'まだないよ(笑)'}
  93. </Tab>
  94. {['admin', 'member'].some (r => user?.role === r) && (
  95. <Tab name="編輯">
  96. <PostEditForm post={post}
  97. onSave={newPost => {
  98. qc.setQueryData (['posts', String (id)],
  99. (prev: any) => newPost ?? prev)
  100. qc.invalidateQueries ({ queryKey: ['posts', 'index'] })
  101. qc.invalidateQueries ({ queryKey: ['related', String (id)] })
  102. toast ({ description: '更新しました.' })
  103. }}/>
  104. </Tab>)}
  105. </TabGroup>
  106. </>)
  107. : 'Loading...'}
  108. </MainArea>
  109. <div className="md:hidden">
  110. <TagDetailSidebar post={post ?? null}/>
  111. </div>
  112. </div>)
  113. }) satisfies FC<Props>