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

184 lines
4.7 KiB

  1. import { useEffect, useState } from 'react'
  2. import { Helmet } from 'react-helmet-async'
  3. import { useParams } from 'react-router-dom'
  4. import TagLink from '@/components/TagLink'
  5. import WikiBody from '@/components/WikiBody'
  6. import Label from '@/components/common/Label'
  7. import PageTitle from '@/components/common/PageTitle'
  8. import TabGroup, { Tab } from '@/components/common/TabGroup'
  9. import TagInput from '@/components/common/TagInput'
  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 { apiGet, apiPut } from '@/lib/api'
  15. import type { FC } from 'react'
  16. import type { Material, Tag } from '@/types'
  17. type MaterialWithTag = Material & { tag: Tag }
  18. const MaterialDetailPage: FC = () => {
  19. const { id } = useParams ()
  20. const [file, setFile] = useState<File | null> (null)
  21. const [filePreview, setFilePreview] = useState ('')
  22. const [loading, setLoading] = useState (false)
  23. const [material, setMaterial] = useState<MaterialWithTag | null> (null)
  24. const [sending, setSending] = useState (false)
  25. const [tag, setTag] = useState ('')
  26. const [url, setURL] = useState ('')
  27. const handleSubmit = async () => {
  28. const formData = new FormData
  29. if (tag.trim ())
  30. formData.append ('tag', tag)
  31. if (file)
  32. formData.append ('file', file)
  33. if (url.trim ())
  34. formData.append ('url', url)
  35. try
  36. {
  37. setSending (true)
  38. const data = await apiPut<Material> (`/materials/${ id }`, formData)
  39. setMaterial (data)
  40. toast ({ title: '更新成功!' })
  41. }
  42. catch
  43. {
  44. toast ({ title: '更新失敗……', description: '入力を見直してください.' })
  45. }
  46. finally
  47. {
  48. setSending (false)
  49. }
  50. }
  51. useEffect (() => {
  52. if (!(id))
  53. return
  54. void (async () => {
  55. try
  56. {
  57. setLoading (true)
  58. const data = await apiGet<MaterialWithTag> (`/materials/${ id }`)
  59. setMaterial (data)
  60. setTag (data.tag.name)
  61. if (data.file && data.contentType)
  62. {
  63. setFilePreview (data.file)
  64. setFile (new File ([await (await fetch (data.file)).blob ()],
  65. data.file,
  66. { type: data.contentType }))
  67. }
  68. setURL (data.url ?? '')
  69. }
  70. finally
  71. {
  72. setLoading (false)
  73. }
  74. }) ()
  75. }, [id])
  76. return (
  77. <MainArea>
  78. {material && (
  79. <Helmet>
  80. <title>{`${ material.tag.name } 素材照会 | ${ SITE_TITLE }`}</title>
  81. </Helmet>)}
  82. {loading ? 'Loading...' : (material && (
  83. <>
  84. <PageTitle>
  85. <TagLink
  86. tag={material.tag}
  87. withWiki={false}
  88. withCount={false}/>
  89. </PageTitle>
  90. {(material.file && material.contentType) && (
  91. (/image\/.*/.test (material.contentType) && (
  92. <img src={material.file} alt={material.tag.name || undefined}/>))
  93. || (/video\/.*/.test (material.contentType) && (
  94. <video src={material.file} controls/>))
  95. || (/audio\/.*/.test (material.contentType) && (
  96. <audio src={material.file} controls/>)))}
  97. <TabGroup>
  98. <Tab name="Wiki">
  99. <WikiBody
  100. title={material.tag.name}
  101. body={material.wikiPageBody ?? undefined}/>
  102. </Tab>
  103. <Tab name="編輯">
  104. <div className="max-w-wl pt-2 space-y-4">
  105. {/* タグ */}
  106. <div>
  107. <Label>タグ</Label>
  108. <TagInput value={tag} setValue={setTag}/>
  109. </div>
  110. {/* ファイル */}
  111. <div>
  112. <Label>ファイル</Label>
  113. <input
  114. type="file"
  115. accept="image/*,video/*,audio/*"
  116. onChange={e => {
  117. const f = e.target.files?.[0]
  118. setFile (f ?? null)
  119. setFilePreview (f ? URL.createObjectURL (f) : '')
  120. }}/>
  121. {(file && filePreview) && (
  122. (/image\/.*/.test (file.type) && (
  123. <img
  124. src={filePreview}
  125. alt="preview"
  126. className="mt-2 max-h-48 rounded border"/>))
  127. || (/video\/.*/.test (file.type) && (
  128. <video
  129. src={filePreview}
  130. controls
  131. className="mt-2 max-h-48 rounded border"/>))
  132. || (/audio\/.*/.test (file.type) && (
  133. <audio
  134. src={filePreview}
  135. controls
  136. className="mt-2 max-h-48"/>))
  137. || (
  138. <p className="text-red-600 dark:text-red-400">
  139. その形式のファイルには対応していません.
  140. </p>))}
  141. </div>
  142. {/* 参考 URL */}
  143. <div>
  144. <Label>参考 URL</Label>
  145. <input
  146. type="url"
  147. value={url}
  148. onChange={e => setURL (e.target.value)}
  149. className="w-full border p-2 rounded"/>
  150. </div>
  151. {/* 送信 */}
  152. <Button
  153. onClick={handleSubmit}
  154. className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"
  155. disabled={sending}>
  156. 更新
  157. </Button>
  158. </div>
  159. </Tab>
  160. </TabGroup>
  161. </>))}
  162. </MainArea>)
  163. }
  164. export default MaterialDetailPage