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 TextArea from '@/components/common/TextArea' 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 { appendPostNewSessionId, serialisePostNewState, } from '@/lib/postNewQueryState' import { canEditContent } from '@/lib/users' import { countImportSourceLines, generatePostImportSessionId, cleanupExpiredPostImportSessions, initialisePreviewRows, loadPostImportSession, loadPostImportSourceDraft, resultRepairMode, serialisedPostImportRowsEqual, 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 buildInitialRows = (source: string): PostImportRow[] => source .split (/\r\n|\n|\r/) .map ((line, index) => ({ sourceRow: index + 1, url: line.trim () })) .filter (row => row.url !== '') .map (row => ({ sourceRow: row.sourceRow, url: row.url, attributes: { title: '', thumbnailBase: '', originalCreatedFrom: '', originalCreatedBefore: '', duration: '', videoMs: '', tags: '', parentPostIds: '' }, fieldWarnings: { }, baseWarnings: [], validationErrors: { }, provenance: { url: 'manual', title: 'automatic', thumbnailBase: 'automatic', originalCreatedFrom: 'automatic', originalCreatedBefore: 'automatic', duration: 'automatic', videoMs: 'automatic', tags: 'automatic', parentPostIds: 'automatic' }, tagSources: { automatic: '', manual: '' }, status: 'pending' as const, displayTags: [], resetSnapshot: { url: row.url, attributes: { title: '', thumbnailBase: '', originalCreatedFrom: '', originalCreatedBefore: '', duration: '', videoMs: '', tags: '', parentPostIds: '' }, provenance: { url: 'manual', title: 'automatic', thumbnailBase: 'automatic', originalCreatedFrom: 'automatic', originalCreatedBefore: 'automatic', duration: 'automatic', videoMs: 'automatic', tags: 'automatic', parentPostIds: 'automatic' }, tagSources: { automatic: '', manual: '' }, displayTags: [], fieldWarnings: { }, baseWarnings: [] } })) 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 (value => value != null) .join (' ') const showStorageError = (message: string) => toast ({ description: message }) 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 nextRows = initialisePreviewRows (buildInitialRows (source)) const serialised = await serialisePostNewState (nextRows) const sessionId = (() => { try { return generatePostImportSessionId () } catch { return null } }) () const session = { source, rows: nextRows, repairMode: resultRepairMode (nextRows) } const saved = sessionId == null ? false : savePostImportSession (sessionId, session, showStorageError) const loadedSession = sessionId == null ? null : loadPostImportSession (sessionId, showStorageError) const sessionRoundTripSucceeded = saved && serialisedPostImportRowsEqual ( loadedSession?.rows ?? [], nextRows) const canNavigate = serialised?.complete === true || sessionRoundTripSucceeded if (!(canNavigate)) { showStorageError ('ブラウザへ保存できませんでした.') return } const nextPath = (() => { if (serialised != null) { return sessionId == null ? serialised.path : appendPostNewSessionId (serialised.path, sessionId) } if (sessionRoundTripSucceeded && sessionId != null) return `/posts/new?session_id=${ sessionId }` return null }) () if (nextPath == null) { showStorageError ('ブラウザへ保存できませんでした.') return } navigate (nextPath) } catch (error) { console.error (error) showStorageError ('ブラウザへ保存できませんでした.') } finally { setLoading (false) } } if (!(editable)) return return ( {`広場に投稿を追加 | ${ SITE_TITLE }`}
広場に投稿を追加 {() => (