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

158 lines
5.5 KiB

  1. import React, { useEffect, useState } from 'react'
  2. import { Link, useLocation, useParams, useNavigate } from 'react-router-dom'
  3. import axios from 'axios'
  4. import { API_BASE_URL, SITE_TITLE } from '../config'
  5. import NicoViewer from '../components/NicoViewer'
  6. import { Button } from '@/components/ui/button'
  7. import { toast } from '@/components/ui/use-toast'
  8. import { cn } from '@/lib/utils'
  9. type Tag = { id: number
  10. name: string
  11. category: string }
  12. type Post = { id: number
  13. url: string
  14. title: string
  15. thumbnail: string
  16. tags: Tag[]
  17. viewed: boolean }
  18. type Props = { posts: Post[]
  19. setPosts: (posts: Post[]) => void }
  20. const PostNewPage = () => {
  21. const location = useLocation ()
  22. const navigate = useNavigate ()
  23. const [title, setTitle] = useState ('')
  24. const [titleAutoFlg, setTitleAutoFlg] = useState (true)
  25. const [url, setURL] = useState ('')
  26. const [thumbnailFile, setThumbnailFile] = useState<File | null> (null)
  27. const [thumbnailPreview, setThumbnailPreview] = useState<string> ('')
  28. const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true)
  29. const [tags, setTags] = useState<Tag[]> ([])
  30. const [tagIds, setTagIds] = useState<number[]> ([])
  31. const handleSubmit = () => {
  32. const formData = new FormData ()
  33. formData.append ('title', title || null)
  34. formData.append ('url', url)
  35. formData.append ('tags', JSON.stringify (tagIds))
  36. formData.append ('thumbnail', thumbnailAutoFlg ? null : thumbnailFile)
  37. void (axios.post (`${ API_BASE_URL }/posts`, formData, { headers: {
  38. 'Content-Type': 'multipart/form-data',
  39. 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
  40. .then (() => {
  41. toast ({ title: '投稿成功!' })
  42. navigate ('/posts')
  43. })
  44. .catch (e => toast ({ title: '投稿失敗',
  45. description: '入力を確認してください。' })))
  46. }
  47. document.title = `広場に投稿を追加 | ${ SITE_TITLE }`
  48. useEffect (() => {
  49. void (axios.get ('/api/tags')
  50. .then (res => setTags (res.data))
  51. .catch (() => toast ({ title: 'タグ一覧の取得失敗' })))
  52. }, [])
  53. return (
  54. <div className="max-w-xl mx-auto p-4 space-y-4">
  55. <h1 className="text-2xl font-bold mb-2">広場に投稿を追加する</h1>
  56. {/* URL */}
  57. <div>
  58. <label className="block font-semibold mb-1">URL</label>
  59. <input type="text"
  60. placeholder="例:https://www.nicovideo.jp/watch/..."
  61. value={url}
  62. onChange={e => setURL (e.target.value)}
  63. className="w-full border p-2 rounded" />
  64. </div>
  65. {/* タイトル */}
  66. <div>
  67. <div className="flex gap-2 mb-1">
  68. <label className="flex-1 block font-semibold">タイトル</label>
  69. <label className="flex items-center block gap-1">
  70. <input type="checkbox"
  71. checked={titleAutoFlg}
  72. onChange={e => setTitleAutoFlg (e.target.checked)} />
  73. 自動
  74. </label>
  75. </div>
  76. <input type="text"
  77. className="w-full border rounded p-2"
  78. value={title}
  79. onChange={e => setTitle (e.target.value)}
  80. disabled={titleAutoFlg} />
  81. </div>
  82. {/* サムネール */}
  83. <div>
  84. <div className="flex gap-2 mb-1">
  85. <label className="block font-semibold flex-1">サムネール</label>
  86. <label className="flex items-center gap-1">
  87. <input type="checkbox"
  88. checked={thumbnailAutoFlg}
  89. onChange={e => setThumbnailAutoFlg (e.target.checked)} />
  90. 自動
  91. </label>
  92. </div>
  93. {thumbnailAutoFlg
  94. ? (
  95. <p className="text-gray-500 text-sm">
  96. URL から自動取得されます。
  97. </p>)
  98. : (
  99. <>
  100. <input type="file"
  101. accept="image/*"
  102. onChange={e => {
  103. const file = e.target.files?.[0]
  104. if (file)
  105. {
  106. setThumbnailFile (file)
  107. setThumbnailPreview (URL.createObjectURL (file))
  108. }
  109. }} />
  110. {thumbnailPreview && (
  111. <img src={thumbnailPreview}
  112. alt="preview"
  113. className="mt-2 max-h-48 rounded border" />)}
  114. </>)}
  115. </div>
  116. {/* タグ */}
  117. <div>
  118. <label className="block font-semibold">タグ</label>
  119. <select multiple
  120. value={tagIds.map (String)}
  121. onChange={e => {
  122. const values = Array.from (e.target.selectedOptions).map (o => Number (o.value))
  123. setTagIds (values)
  124. }}
  125. className="w-full p-2 border rounded h-32">
  126. {tags.map ((tag: Tag) => (
  127. <option key={tag.id} value={tag.id}>
  128. {tag.name}
  129. </option>))}
  130. </select>
  131. </div>
  132. {/* 送信 */}
  133. <button onClick={handleSubmit}
  134. className="px-4 py-2 bg-blue-600 text-white rounded">
  135. 追加
  136. </button>
  137. </div>)
  138. }
  139. export default PostNewPage