ファイル
btrc-hub/frontend/src/pages/materials/MaterialNewPage.tsx
T
2026-10-18 02:45:26 +09:00

174 行
5.3 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import TagInput from '@/components/common/TagInput'
import MainArea from '@/components/layout/MainArea'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { createMaterial } from '@/lib/materials'
import { unlocalisedPath } from '@/lib/locales'
import { materialsKeys } from '@/lib/queryKeys'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react'
type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialNewPage: FC = () => {
const locale = useLocale ()
const { message, label } = useUITranslation ('materials')
const qc = useQueryClient ()
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagQuery = query.get ('tag') ?? ''
const returnToQuery = query.get ('return_to') ?? ''
const safeReturnTo =
unlocalisedPath (returnToQuery).startsWith ('/materials') ? returnToQuery : ''
const navigate = useLocaleNavigate ()
const [file, setFile] = useState<File | null> (null)
const [filePreview, setFilePreview] = useState ('')
const [tag, setTag] = useState (tagQuery)
const [url, setURL] = useState ('')
const [exportPath, setExportPath] = useState ('')
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
useValidationErrors<MaterialFormField> ()
const createMutation = useMutation ({
mutationFn: async () => {
const formData = new FormData
if (tag)
formData.append ('tag', tag)
if (file)
formData.append ('file', file)
if (url)
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await createMaterial (formData, locale)
},
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
toast ({ title: label ('sent') })
navigate (safeReturnTo || '/materials')
},
onError: error => {
applyValidationError (error)
toast ({ title: label ('sendFailed'), description: label ('shared:messages.reviewInput') })
}})
const handleSubmit = () => {
clearValidationErrors ()
createMutation.mutate ()
}
return (
<MainArea>
<Helmet>
<title>{message ('newTitle').text} | {SITE_TITLE}</title>
</Helmet>
<Form>
<PageTitle>{label ('newTitle')}</PageTitle>
<FieldError messages={baseErrors}/>
{/* タグ */}
<FormField label={label ('navigation:tags')} messages={fieldErrors.tag}>
{({ describedBy, invalid }) => (
<TagInput
describedBy={describedBy}
invalid={invalid}
value={tag}
setValue={setTag}/>)}
</FormField>
{/* ファイル */}
<FormField label={label ('file')} 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={message ('preview').text}
lang={message ('preview').language}
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">
{label ('unsupportedFile')}
</p>))}
</>)}
</FormField>
{/* 参考 URL */}
<FormField label={label ('referenceUrl')} 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>
<FormField
label={label ('exportPath')}
messages={fieldErrors.exportPaths}>
{({ describedBy, invalid }) => (
<input
type="text"
value={exportPath}
onChange={e => setExportPath (e.target.value)}
placeholder={message ('exportPlaceholder').text}
lang={message ('exportPlaceholder').language}
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}/>)}
</FormField>
{/* 送信 */}
<Button
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"
disabled={createMutation.isPending}>
{label ('shared:actions.add')}
</Button>
</Form>
</MainArea>)
}
export default MaterialNewPage