Reviewed-on: #355 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #355 でマージされました.
このコミットが含まれているのは:
@@ -4,7 +4,8 @@ import { useParams } from 'react-router-dom'
|
||||
|
||||
import TagLink from '@/components/TagLink'
|
||||
import WikiBody from '@/components/WikiBody'
|
||||
import Label from '@/components/common/Label'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||
import TagInput from '@/components/common/TagInput'
|
||||
@@ -13,6 +14,8 @@ import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, apiPut } from '@/lib/api'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
import { useValidationErrors } from '@/lib/useValidationErrors'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
@@ -20,6 +23,8 @@ import type { Material, Tag } from '@/types'
|
||||
|
||||
type MaterialWithTag = Material & { tag: Tag }
|
||||
|
||||
type MaterialFormField = 'tag' | 'file' | 'url'
|
||||
|
||||
|
||||
const MaterialDetailPage: FC = () => {
|
||||
const { id } = useParams ()
|
||||
@@ -31,8 +36,12 @@ const MaterialDetailPage: FC = () => {
|
||||
const [sending, setSending] = useState (false)
|
||||
const [tag, setTag] = useState ('')
|
||||
const [url, setURL] = useState ('')
|
||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||
useValidationErrors<MaterialFormField> ()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
clearValidationErrors ()
|
||||
|
||||
const formData = new FormData
|
||||
if (tag.trim ())
|
||||
formData.append ('tag', tag)
|
||||
@@ -48,8 +57,9 @@ const MaterialDetailPage: FC = () => {
|
||||
setMaterial (data)
|
||||
toast ({ title: '更新成功!' })
|
||||
}
|
||||
catch
|
||||
catch (e)
|
||||
{
|
||||
applyValidationError (e)
|
||||
toast ({ title: '更新失敗……', description: '入力を見直してください.' })
|
||||
}
|
||||
finally
|
||||
@@ -118,54 +128,66 @@ const MaterialDetailPage: FC = () => {
|
||||
|
||||
<Tab name="編輯">
|
||||
<div className="max-w-wl pt-2 space-y-4">
|
||||
<FieldError messages={baseErrors}/>
|
||||
|
||||
{/* タグ */}
|
||||
<div>
|
||||
<Label>タグ</Label>
|
||||
<TagInput value={tag} setValue={setTag}/>
|
||||
</div>
|
||||
<FormField label="タグ" messages={fieldErrors.tag}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<TagInput
|
||||
describedBy={describedBy}
|
||||
invalid={invalid}
|
||||
value={tag}
|
||||
setValue={setTag}/>)}
|
||||
</FormField>
|
||||
|
||||
{/* ファイル */}
|
||||
<div>
|
||||
<Label>ファイル</Label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,video/*,audio/*"
|
||||
onChange={e => {
|
||||
const f = e.target.files?.[0]
|
||||
setFile (f ?? null)
|
||||
setFilePreview (f ? URL.createObjectURL (f) : '')
|
||||
}}/>
|
||||
{(file && filePreview) && (
|
||||
(/image\/.*/.test (file.type) && (
|
||||
<img
|
||||
src={filePreview}
|
||||
alt="preview"
|
||||
className="mt-2 max-h-48 rounded border"/>))
|
||||
|| (/video\/.*/.test (file.type) && (
|
||||
<video
|
||||
src={filePreview}
|
||||
controls
|
||||
className="mt-2 max-h-48 rounded border"/>))
|
||||
|| (/audio\/.*/.test (file.type) && (
|
||||
<audio
|
||||
src={filePreview}
|
||||
controls
|
||||
className="mt-2 max-h-48"/>))
|
||||
|| (
|
||||
<p className="text-red-600 dark:text-red-400">
|
||||
その形式のファイルには対応していません.
|
||||
</p>))}
|
||||
</div>
|
||||
<FormField label="ファイル" messages={fieldErrors.file}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,video/*,audio/*"
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
onChange={e => {
|
||||
const f = e.target.files?.[0]
|
||||
setFile (f ?? null)
|
||||
setFilePreview (f ? URL.createObjectURL (f) : '')
|
||||
}}/>
|
||||
{(file && filePreview) && (
|
||||
(/image\/.*/.test (file.type) && (
|
||||
<img
|
||||
src={filePreview}
|
||||
alt="preview"
|
||||
className="mt-2 max-h-48 rounded border"/>))
|
||||
|| (/video\/.*/.test (file.type) && (
|
||||
<video
|
||||
src={filePreview}
|
||||
controls
|
||||
className="mt-2 max-h-48 rounded border"/>))
|
||||
|| (/audio\/.*/.test (file.type) && (
|
||||
<audio
|
||||
src={filePreview}
|
||||
controls
|
||||
className="mt-2 max-h-48"/>))
|
||||
|| (
|
||||
<p className="text-red-600 dark:text-red-400">
|
||||
その形式のファイルには対応していません.
|
||||
</p>))}
|
||||
</>)}
|
||||
</FormField>
|
||||
|
||||
{/* 参考 URL */}
|
||||
<div>
|
||||
<Label>参考 URL</Label>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={e => setURL (e.target.value)}
|
||||
className="w-full border p-2 rounded"/>
|
||||
</div>
|
||||
<FormField label="参考 URL" messages={fieldErrors.url}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={e => setURL (e.target.value)}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
className={inputClass (invalid)}/>)}
|
||||
</FormField>
|
||||
|
||||
{/* 送信 */}
|
||||
<Button
|
||||
@@ -181,4 +203,4 @@ const MaterialDetailPage: FC = () => {
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
export default MaterialDetailPage
|
||||
export default MaterialDetailPage
|
||||
|
||||
新しい課題から参照
ユーザをブロックする