import { useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useNavigate } from 'react-router-dom' 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 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, cleanupExpiredPostImportSessions, createPostImportSessionId, initialisePreviewRows, loadPostImportSourceDraft, savePostImportSession, savePostImportSourceDraft, validateImportSource } from '@/lib/postImportSession' import Forbidden from '@/pages/Forbidden' import type { FC } from 'react' import type { PostImportRow } from '@/lib/postImportSession' import type { User } from '@/types' type Props = { user: User | null } const MAX_ROWS = 100 const SOURCE_ERROR_ID = 'post-import-source-error' const SOURCE_ISSUES_ID = 'post-import-source-issues' const urlIssuesFromRows = (rows: PostImportRow[], source: string) => { const sourceLines = source.split (/\r\n|\n|\r/) return rows.flatMap (row => (row.validationErrors.url ?? []).map (message => ({ sourceRow: row.sourceRow, message, url: sourceLines[row.sourceRow - 1]?.trim () ?? row.url }))) } const PostImportSourcePage: FC = ({ user }) => { const editable = canEditContent (user) const navigate = useNavigate () const [source, setSource] = useState ('') const [loading, setLoading] = useState (false) const [sourceIssues, setSourceIssues] = useState> ([]) const [sourceError, setSourceError] = useState (null) const saveTimer = useRef (null) const editedRef = useRef (false) const lineCount = countImportSourceLines (source) const messages = sourceError != null ? [sourceError] : [] const sourceDescribedBy = [sourceError != null ? SOURCE_ERROR_ID : null, sourceIssues.length > 0 ? SOURCE_ISSUES_ID : null] .filter (_1 => _1 != null) .join (' ') useEffect (() => { cleanupExpiredPostImportSessions (message => toast ({ title: '保存済みデータを整理できませんでした', description: message })) const draft = loadPostImportSourceDraft (message => toast ({ title: '保存済み入力を復元できませんでした', description: message })) if (!(editedRef.current)) setSource (current => current === '' ? draft.source : current) }, []) useEffect (() => { if (saveTimer.current != null) window.clearTimeout (saveTimer.current) saveTimer.current = window.setTimeout (() => { savePostImportSourceDraft (source, message => toast ({ title: '入力内容を保存できませんでした', description: message })) }, 300) return () => { if (saveTimer.current != null) window.clearTimeout (saveTimer.current) } }, [source]) const preview = async () => { if (lineCount === 0) { setSourceIssues ([]) setSourceError ('URL を入力してください.') return } const issues = validateImportSource (source) if (issues.length > 0) { setSourceIssues (issues) setSourceError (null) return } setLoading (true) setSourceIssues ([]) setSourceError (null) try { const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', { source }) const urlIssues = urlIssuesFromRows (data.rows, source) if (urlIssues.length > 0) { setSourceIssues (urlIssues) return } const sessionId = createPostImportSessionId () const saved = savePostImportSession ( sessionId, { source, rows: initialisePreviewRows (data.rows), repairMode: 'all' }, message => toast ({ title: '取込状態を保存できませんでした', description: message })) if (!(saved)) return 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 setSourceError (message ?? '入力を確認してください.') toast ({ title: '投稿情報の取得に失敗しました', description: message ?? '入力を確認してください.' }) } finally { setLoading (false) } } if (!(editable)) return return ( {`投稿インポート | ${ SITE_TITLE }`}
投稿インポート {() => (