このコミットが含まれているのは:
2026-06-25 08:11:41 +09:00
コミット c10ba7a698
36個のファイルの変更886行の追加1132行の削除
+163 -168
ファイルの表示
@@ -45,8 +45,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
const { data: material, isError, isLoading } = useQuery ({
queryKey: materialsKeys.show (id ?? ''),
queryFn: () => fetchMaterial (id ?? ''),
enabled: id != null,
})
enabled: id != null})
const materialTitle = material
? material.tag?.name ?? `素材 #${ material.id }`
: ''
@@ -60,8 +59,8 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
setExportPath (material.exportPaths.legacyDrive ?? '')
if (material.file && material.contentType)
{
setFilePreview (material.file)
setFile (null)
setFilePreview (material.file)
setFile (null)
}
}, [material])
@@ -71,42 +70,40 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
const updateMutation = useMutation ({
mutationFn: async () => {
const formData = new FormData
if (tag.trim ())
formData.append ('tag', tag)
if (file)
formData.append ('file', file)
if (url.trim ())
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
const formData = new FormData
if (tag.trim ())
formData.append ('tag', tag)
if (file)
formData.append ('file', file)
if (url.trim ())
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await updateMaterial (id ?? '', formData)
return await updateMaterial (id ?? '', formData)
},
onSuccess: async data => {
qc.setQueryData (materialsKeys.show (id ?? ''), data)
await invalidateMaterialQueries ()
toast ({ title: '更新成功!' })
qc.setQueryData (materialsKeys.show (id ?? ''), data)
await invalidateMaterialQueries ()
toast ({ title: '更新成功!' })
},
onError: error => {
applyValidationError (error)
toast ({ title: '更新失敗……', description: '入力を見直してください.' })
},
})
applyValidationError (error)
toast ({ title: '更新失敗……', description: '入力を見直してください.' })
}})
const suppressMutation = useMutation ({
mutationFn: async (reason: string) =>
await suppressMaterialFile (id ?? '', { reason }),
await suppressMaterialFile (id ?? '', { reason }),
onSuccess: async data => {
qc.setQueryData (materialsKeys.show (id ?? ''), data)
setFile (null)
setFilePreview ('')
await invalidateMaterialQueries ()
toast ({ title: '抑止しました' })
qc.setQueryData (materialsKeys.show (id ?? ''), data)
setFile (null)
setFilePreview ('')
await invalidateMaterialQueries ()
toast ({ title: '抑止しました' })
},
onError: () => {
toast ({ title: '抑止に失敗しました' })
},
})
toast ({ title: '抑止に失敗しました' })
}})
const handleSubmit = () => {
clearValidationErrors ()
@@ -125,155 +122,153 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
return (
<MainArea>
{material && (
<Helmet>
<title>{`${ materialTitle } 素材照会 | ${ SITE_TITLE }`}</title>
</Helmet>)}
{material && (
<Helmet>
<title>{`${ materialTitle } 素材照会 | ${ SITE_TITLE }`}</title>
</Helmet>)}
{isLoading ? 'Loading...' : isError ? (
<p className="text-red-600 dark:text-red-300">
</p>
) : material == null ? (
<p className="text-stone-700 dark:text-stone-300">
</p>
) : (
<>
<PageTitle>
{material.tag
? (
<TagLink
tag={material.tag}
withWiki={false}
withCount={false}/>)
: materialTitle}
</PageTitle>
{isLoading ? 'Loading...' : isError ? (
<p className="text-red-600 dark:text-red-300">
</p>) : material == null ? (
<p className="text-stone-700 dark:text-stone-300">
</p>) : (
<>
<PageTitle>
{material.tag
? (
<TagLink
tag={material.tag}
withWiki={false}
withCount={false}/>)
: materialTitle}
</PageTitle>
{material.fileSuppressedAt && (
<div className="mb-4 rounded border border-red-300 bg-red-50 p-3
text-red-900 dark:border-red-800 dark:bg-red-950
dark:text-red-100">
<span></span>
{material.fileSuppressionReason && (
<span> : {material.fileSuppressionReason}</span>)}
</div>)}
{material.fileSuppressedAt && (
<div className="mb-4 rounded border border-red-300 bg-red-50 p-3
text-red-900 dark:border-red-800 dark:bg-red-950
dark:text-red-100">
<span></span>
{material.fileSuppressionReason && (
<span> : {material.fileSuppressionReason}</span>)}
</div>)}
{(!material.fileSuppressedAt && material.file && material.contentType) && (
(/image\/.*/.test (material.contentType) && (
<img src={material.file} alt={material.tag?.name || undefined}/>))
|| (/video\/.*/.test (material.contentType) && (
<video src={material.file} controls/>))
|| (/audio\/.*/.test (material.contentType) && (
<audio src={material.file} controls/>)))}
{(!material.fileSuppressedAt && material.file && material.contentType) && (
(/image\/.*/.test (material.contentType) && (
<img src={material.file} alt={material.tag?.name || undefined}/>))
|| (/video\/.*/.test (material.contentType) && (
<video src={material.file} controls/>))
|| (/audio\/.*/.test (material.contentType) && (
<audio src={material.file} controls/>)))}
<TabGroup>
<Tab name="Wiki">
{material.tag
? (
<WikiBody
title={material.tag.name}
body={material.wikiPageBody ?? undefined}/>)
: (
<p className="text-stone-700 dark:text-stone-300">
</p>)}
</Tab>
<TabGroup>
<Tab name="Wiki">
{material.tag
? (
<WikiBody
title={material.tag.name}
body={material.wikiPageBody ?? undefined}/>)
: (
<p className="text-stone-700 dark:text-stone-300">
</p>)}
</Tab>
<Tab name="編輯">
<div className="max-w-wl space-y-4 pt-2">
<FieldError messages={baseErrors}/>
<Tab name="編輯">
<div className="max-w-wl space-y-4 pt-2">
<FieldError messages={baseErrors}/>
<FormField label="タグ" messages={fieldErrors.tag}>
{({ describedBy, invalid }) => (
<TagInput
describedBy={describedBy}
invalid={invalid}
value={tag}
setValue={setTag}/>)}
</FormField>
<FormField label="タグ" messages={fieldErrors.tag}>
{({ describedBy, invalid }) => (
<TagInput
describedBy={describedBy}
invalid={invalid}
value={tag}
setValue={setTag}/>)}
</FormField>
<FormField label="ファイル" messages={fieldErrors.file}>
{({ describedBy, invalid }) => (
<>
<input
type="file"
accept="image/*,video/*,audio/*"
aria-describedby={describedBy}
aria-invalid={invalid}
onChange={e => {
const nextFile = e.target.files?.[0]
setFile (nextFile ?? null)
setFilePreview (
nextFile ? URL.createObjectURL (nextFile) : '')
}}/>
{(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>
<FormField label="ファイル" messages={fieldErrors.file}>
{({ describedBy, invalid }) => (
<>
<input
type="file"
accept="image/*,video/*,audio/*"
aria-describedby={describedBy}
aria-invalid={invalid}
onChange={e => {
const nextFile = e.target.files?.[0]
setFile (nextFile ?? null)
setFilePreview (
nextFile ? URL.createObjectURL (nextFile) : '')
}}/>
{(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>
<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>
<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>
<FormField label="ZIP 出力パス" messages={fieldErrors.exportPaths}>
{({ describedBy, invalid }) => (
<input
type="text"
value={exportPath}
onChange={e => setExportPath (e.target.value)}
placeholder="伊地知ニジカ/表情/泣き.png"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="ZIP 出力パス" messages={fieldErrors.exportPaths}>
{({ describedBy, invalid }) => (
<input
type="text"
value={exportPath}
onChange={e => setExportPath (e.target.value)}
placeholder="伊地知ニジカ/表情/泣き.png"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}/>)}
</FormField>
<div className="flex flex-wrap gap-2">
<Button
onClick={handleSubmit}
className="rounded bg-blue-600 px-4 py-2 text-white
disabled:bg-gray-400"
disabled={updateMutation.isPending}>
</Button>
{user?.role === 'admin' && !material.fileSuppressedAt && (
<Button
type="button"
variant="destructive"
onClick={handleSuppress}
disabled={suppressMutation.isPending}>
</Button>)}
</div>
</div>
</Tab>
</TabGroup>
</>)}
<div className="flex flex-wrap gap-2">
<Button
onClick={handleSubmit}
className="rounded bg-blue-600 px-4 py-2 text-white
disabled:bg-gray-400"
disabled={updateMutation.isPending}>
</Button>
{user?.role === 'admin' && !material.fileSuppressedAt && (
<Button
type="button"
variant="destructive"
onClick={handleSuppress}
disabled={suppressMutation.isPending}>
</Button>)}
</div>
</div>
</Tab>
</TabGroup>
</>)}
</MainArea>)
}