このコミットが含まれているのは:
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
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 { apiPost, isApiError } from '@/lib/api'
|
||||
import { canEditContent } from '@/lib/users'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
import { countImportSourceLines,
|
||||
countImportSourceValidLines,
|
||||
createPostImportSessionId,
|
||||
loadPostImportSourceDraft,
|
||||
savePostImportSession,
|
||||
savePostImportSourceDraft,
|
||||
type PostImportRow } from '@/lib/postImportSession'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
const MAX_ROWS = 100
|
||||
|
||||
|
||||
const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
const navigate = useNavigate ()
|
||||
const draft = useMemo (() => loadPostImportSourceDraft (), [])
|
||||
|
||||
const [source, setSource] = useState (draft.source)
|
||||
const [existing, setExisting] = useState<'skip' | 'error'> (draft.existing)
|
||||
const [loading, setLoading] = useState (false)
|
||||
const [error, setError] = useState<string | null> (null)
|
||||
|
||||
const lineCount = countImportSourceLines (source)
|
||||
const validLineCount = countImportSourceValidLines (source)
|
||||
const messages = [
|
||||
...(lineCount === 0 ? ['URL を入力してください.'] : []),
|
||||
...(lineCount > MAX_ROWS ? [`最大 ${ MAX_ROWS } 件までです.`] : []),
|
||||
...(error ? [error] : [])]
|
||||
|
||||
useEffect (() => {
|
||||
savePostImportSourceDraft (source, existing)
|
||||
}, [existing, source])
|
||||
|
||||
const preview = async () => {
|
||||
setLoading (true)
|
||||
setError (null)
|
||||
try
|
||||
{
|
||||
const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', {
|
||||
source,
|
||||
existing })
|
||||
const sessionId = createPostImportSessionId ()
|
||||
savePostImportSession (sessionId, {
|
||||
source,
|
||||
existing,
|
||||
rows: data.rows,
|
||||
repairMode: 'all' })
|
||||
navigate (`/posts/import/${ sessionId }/review`)
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
const message =
|
||||
isApiError<{ message?: string, baseErrors?: string[] }> (requestError)
|
||||
? requestError.response?.data?.message
|
||||
?? requestError.response?.data?.baseErrors?.[0]
|
||||
: undefined
|
||||
setError (message ?? '入力を確認してください.')
|
||||
toast ({
|
||||
title: '投稿情報の取得に失敗しました',
|
||||
description: message ?? '入力を確認してください.' })
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading (false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!(editable))
|
||||
return <Forbidden/>
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`投稿インポート | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="mx-auto max-w-4xl space-y-4 p-4">
|
||||
<PageTitle>投稿インポート</PageTitle>
|
||||
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
|
||||
dark:bg-neutral-900 md:p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold">URL リストを入力</h2>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
一行につき一つの URL を入力してください.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormField label="URL リスト">
|
||||
{() => (
|
||||
<textarea
|
||||
value={source}
|
||||
rows={14}
|
||||
className={inputClass (
|
||||
messages.length > 0,
|
||||
'font-mono text-sm',
|
||||
)}
|
||||
onChange={ev => setSource (ev.target.value)}/>)}
|
||||
</FormField>
|
||||
<FieldError messages={messages}/>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 text-sm
|
||||
text-neutral-600 dark:text-neutral-300">
|
||||
<div>有効行数: {validLineCount}</div>
|
||||
<div>最大 {MAX_ROWS} 件</div>
|
||||
</div>
|
||||
|
||||
<label className="block space-y-1">
|
||||
<span className="text-sm font-medium">既存投稿</span>
|
||||
<select
|
||||
value={existing}
|
||||
onChange={ev => setExisting (ev.target.value as typeof existing)}
|
||||
className={inputClass (false, 'w-full md:w-auto')}>
|
||||
<option value="skip">スキップ</option>
|
||||
<option value="error">エラー</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={preview}
|
||||
disabled={loading || lineCount === 0 || lineCount > MAX_ROWS}>
|
||||
投稿情報を取得
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
export default PostImportSourcePage
|
||||
新しい課題から参照
ユーザをブロックする