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

199 行
6.5 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
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 { createMaterialSyncSuppression, fetchMaterialSyncSuppressions } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys'
import { localisedDateString, inputClass } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC, FormEvent } from 'react'
import type { MaterialSyncSuppressionSourceKind } from '@/types'
const SOURCE_KIND_LABELS: Record<MaterialSyncSuppressionSourceKind, string> = {
uri: 'suppressions.sources.uri',
google_drive_path: 'suppressions.sources.google_drive_path',
google_drive_path_prefix: 'suppressions.sources.google_drive_path_prefix',
google_drive_file: 'suppressions.sources.google_drive_file',
legacy_drive_path: 'suppressions.sources.legacy_drive_path',
legacy_drive_path_prefix: 'suppressions.sources.legacy_drive_path_prefix' }
const REASONS = [
'copyright_high_risk',
'copyright_takedown',
'adult_or_sensitive',
'personal_information',
'malware_or_dangerous_file',
'duplicate_or_low_quality',
'source_owner_request',
'other'] as const
type MaterialSyncSuppressionReason = typeof REASONS[number]
const MaterialSyncSuppressionsPage: FC = () => {
const locale = useLocale ()
const { message, label } = useUITranslation ('materials')
const qc = useQueryClient ()
const [sourceKind, setSourceKind] =
useState<MaterialSyncSuppressionSourceKind> ('uri')
const [sourceUri, setSourceUri] = useState ('')
const [drivePath, setDrivePath] = useState ('')
const [driveFileId, setDriveFileId] = useState ('')
const [reason, setReason] = useState<MaterialSyncSuppressionReason> (REASONS[0])
const { data, isError, isLoading } = useQuery ({
queryKey: materialsKeys.suppressions (locale),
queryFn: () => fetchMaterialSyncSuppressions (locale)})
const createMutation = useMutation ({
mutationFn: async () =>
await createMaterialSyncSuppression ({
sourceKind,
sourceUri,
drivePath,
driveFileId,
reason}, locale),
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions (locale) })
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
setSourceUri ('')
setDrivePath ('')
setDriveFileId ('')
toast ({ title: label ('suppressions.registered') })
},
onError: () => {
toast ({ title: label ('suppressions.registerFailed') })
}})
const handleSubmit = (event: FormEvent) => {
event.preventDefault ()
createMutation.mutate ()
}
const suppressions = data?.suppressions ?? []
return (
<MainArea>
<Helmet>
<title>{message ('suppressions.title').text} | {SITE_TITLE}</title>
</Helmet>
<div className="space-y-5">
<PageTitle>{label ('suppressions.heading')}</PageTitle>
<form
onSubmit={handleSubmit}
className="max-w-3xl rounded-lg border border-stone-200 bg-white p-4
text-stone-900 dark:border-stone-700 dark:bg-stone-900
dark:text-stone-100">
<div className="space-y-3">
<FormField label={label ('suppressions.sourceKind')}>
{({ invalid }) => (
<select
value={sourceKind}
onChange={e => setSourceKind (
e.target.value as MaterialSyncSuppressionSourceKind)}
className={inputClass (invalid)}>
{Object.entries (SOURCE_KIND_LABELS).map (([value, key]) => (
<option key={value} value={value} lang={message (key).language}>
{message (key).text}
</option>))}
</select>)}
</FormField>
<FormField label={label ('suppressions.sourceUri')}>
{({ invalid }) => (
<input
type="text"
value={sourceUri}
onChange={e => setSourceUri (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label={label ('suppressions.drivePath')}>
{({ invalid }) => (
<input
type="text"
value={drivePath}
onChange={e => setDrivePath (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label={label ('suppressions.driveFileId')}>
{({ invalid }) => (
<input
type="text"
value={driveFileId}
onChange={e => setDriveFileId (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label={label ('suppressions.reason')}>
{({ invalid }) => (
<select
value={reason}
onChange={e => setReason (e.target.value as MaterialSyncSuppressionReason)}
className={inputClass (invalid)}>
{REASONS.map (value => (
<option key={value} value={value}
lang={message (`suppressions.reasons.${value}`).language}>
{message (`suppressions.reasons.${value}`).text}
</option>))}
</select>)}
</FormField>
</div>
<Button
type="submit"
className="mt-4 rounded bg-blue-600 px-4 py-2 text-white
disabled:bg-gray-400"
disabled={createMutation.isPending}>
{label ('suppressions.register')}
</Button>
</form>
{isLoading && <p>{label ('common:loading')}</p>}
{isError && (
<p className="text-red-600 dark:text-red-300">
{label ('suppressions.error')}
</p>)}
<div className="space-y-3">
{suppressions.map (suppression => (
<article
key={suppression.id}
className="rounded-lg border border-stone-200 bg-white p-3
text-stone-900 shadow-sm dark:border-stone-700
dark:bg-stone-900 dark:text-stone-100">
<div className="font-medium">
{label (SOURCE_KIND_LABELS[suppression.sourceKind])}
</div>
<div className="break-all text-sm text-stone-700 dark:text-stone-300">
{suppression.normalizedSourceKey}
</div>
<div className="mt-2 text-sm text-stone-600 dark:text-stone-400">
{label ('suppressions.reasonPrefix')}
{REASONS.includes (suppression.reason as MaterialSyncSuppressionReason)
? label (`suppressions.reasons.${suppression.reason}`)
: suppression.reason} /{' '}
{label ('suppressions.registeredPrefix')}
<time dateTime={suppression.createdAt} lang={locale}>
{localisedDateString (suppression.createdAt, locale)}
</time>
</div>
</article>))}
</div>
</div>
</MainArea>)
}
export default MaterialSyncSuppressionsPage