174 行
5.3 KiB
TypeScript
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
|