'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { DocumentArrowUpIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'; import Button from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import ImportLayout from '@/components/layout/ImportLayout'; import AuthorSelector from '@/components/stories/AuthorSelector'; import SeriesSelector from '@/components/stories/SeriesSelector'; type FileType = 'epub' | 'pdf' | 'zip' | null; interface ImportResponse { success: boolean; message: string; storyId?: string; storyTitle?: string; fileName?: string; fileType?: string; wordCount?: number; extractedImages?: number; warnings?: string[]; errors?: string[]; } interface ZIPAnalysisResponse { success: boolean; message: string; zipFileName?: string; totalFiles?: number; validFiles?: number; files?: FileInfo[]; warnings?: string[]; } interface FileInfo { fileName: string; fileType: string; fileSize: number; extractedTitle?: string; extractedAuthor?: string; hasMetadata: boolean; error?: string; } interface ZIPImportResponse { success: boolean; message: string; totalFiles: number; successfulImports: number; failedImports: number; results: ImportResponse[]; warnings?: string[]; } export default function FileImportPage() { const router = useRouter(); const [selectedFile, setSelectedFile] = useState(null); const [fileType, setFileType] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isValidating, setIsValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [importResult, setImportResult] = useState(null); const [error, setError] = useState(null); // ZIP-specific state const [zipAnalysis, setZipAnalysis] = useState(null); const [zipSessionId, setZipSessionId] = useState(null); const [selectedZipFiles, setSelectedZipFiles] = useState>(new Set()); const [fileMetadata, setFileMetadata] = useState>(new Map()); const [zipImportResult, setZipImportResult] = useState(null); // Import options const [authorName, setAuthorName] = useState(''); const [authorId, setAuthorId] = useState(undefined); const [seriesName, setSeriesName] = useState(''); const [seriesId, setSeriesId] = useState(undefined); const [seriesVolume, setSeriesVolume] = useState(''); const [tags, setTags] = useState(''); const [createMissingAuthor, setCreateMissingAuthor] = useState(true); const [createMissingSeries, setCreateMissingSeries] = useState(true); const [extractImages, setExtractImages] = useState(true); const [preserveReadingPosition, setPreserveReadingPosition] = useState(true); const detectFileType = (file: File): FileType => { const filename = file.name.toLowerCase(); if (filename.endsWith('.epub')) return 'epub'; if (filename.endsWith('.pdf')) return 'pdf'; if (filename.endsWith('.zip')) return 'zip'; return null; }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setSelectedFile(file); setValidationResult(null); setImportResult(null); setZipAnalysis(null); setZipSessionId(null); setSelectedZipFiles(new Set()); setZipImportResult(null); setError(null); const detectedType = detectFileType(file); setFileType(detectedType); if (!detectedType) { setError('Unsupported file type. Please select an EPUB, PDF, or ZIP file.'); return; } if (detectedType === 'zip') { await analyzeZipFile(file); } else { await validateFile(file, detectedType); } } }; const validateFile = async (file: File, type: FileType) => { if (type === 'zip') return; // ZIP has its own analysis flow setIsValidating(true); try { const token = localStorage.getItem('auth-token'); const formData = new FormData(); formData.append('file', file); const endpoint = type === 'epub' ? '/api/stories/epub/validate' : '/api/stories/pdf/validate'; const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': token ? `Bearer ${token}` : '', }, body: formData, }); if (response.ok) { const result = await response.json(); setValidationResult(result); if (!result.valid) { setError(`${type?.toUpperCase() || 'File'} validation failed: ` + result.errors.join(', ')); } } else if (response.status === 401 || response.status === 403) { setError('Authentication required. Please log in.'); } else { setError(`Failed to validate ${type?.toUpperCase() || 'file'}`); } } catch (err) { setError(`Error validating ${type?.toUpperCase() || 'file'}: ` + (err as Error).message); } finally { setIsValidating(false); } }; const analyzeZipFile = async (file: File) => { setIsLoading(true); try { const token = localStorage.getItem('auth-token'); const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/stories/zip/analyze', { method: 'POST', headers: { 'Authorization': token ? `Bearer ${token}` : '', }, body: formData, }); if (response.ok) { const result: ZIPAnalysisResponse = await response.json(); setZipAnalysis(result); if (result.success && result.warnings && result.warnings.length > 0) { // Extract session ID from warnings const sessionWarning = result.warnings.find(w => w.includes('Session ID:')); if (sessionWarning) { const match = sessionWarning.match(/Session ID: ([a-f0-9-]+)/); if (match) { setZipSessionId(match[1]); } } } if (!result.success) { setError(result.message); } else if (result.files && result.files.length === 0) { setError('No valid EPUB or PDF files found in ZIP'); } } else if (response.status === 401 || response.status === 403) { setError('Authentication required. Please log in.'); } else { setError('Failed to analyze ZIP file'); } } catch (err) { setError('Error analyzing ZIP file: ' + (err as Error).message); } finally { setIsLoading(false); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedFile) { setError('Please select a file'); return; } if (fileType === 'zip') { await handleZipImport(); return; } if (validationResult && !validationResult.valid) { setError(`Cannot import invalid ${fileType?.toUpperCase()} file`); return; } // Check PDF requires author if (fileType === 'pdf' && !authorName.trim()) { setError('PDF import requires an author name. Please provide an author name or ensure the PDF has author metadata.'); return; } setIsLoading(true); setError(null); try { const token = localStorage.getItem('auth-token'); const formData = new FormData(); formData.append('file', selectedFile); if (authorId) { formData.append('authorId', authorId); } else if (authorName) { formData.append('authorName', authorName); } if (seriesId) { formData.append('seriesId', seriesId); } else if (seriesName) { formData.append('seriesName', seriesName); } if (seriesVolume) formData.append('seriesVolume', seriesVolume); if (tags) { const tagList = tags.split(',').map(t => t.trim()).filter(t => t.length > 0); tagList.forEach(tag => formData.append('tags', tag)); } formData.append('createMissingAuthor', createMissingAuthor.toString()); formData.append('createMissingSeries', createMissingSeries.toString()); if (fileType === 'epub') { formData.append('preserveReadingPosition', preserveReadingPosition.toString()); } else if (fileType === 'pdf') { formData.append('extractImages', extractImages.toString()); } const endpoint = fileType === 'epub' ? '/api/stories/epub/import' : '/api/stories/pdf/import'; const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': token ? `Bearer ${token}` : '', }, body: formData, }); const result = await response.json(); if (response.ok && result.success) { setImportResult(result); } else if (response.status === 401 || response.status === 403) { setError('Authentication required. Please log in.'); } else { setError(result.message || `Failed to import ${fileType?.toUpperCase()}`); } } catch (err) { setError(`Error importing ${fileType?.toUpperCase()}: ` + (err as Error).message); } finally { setIsLoading(false); } }; const handleZipImport = async () => { if (!zipSessionId) { setError('ZIP session expired. Please re-upload the ZIP file.'); return; } if (selectedZipFiles.size === 0) { setError('Please select at least one file to import'); return; } setIsLoading(true); setError(null); try { const token = localStorage.getItem('auth-token'); const requestBody: any = { zipSessionId: zipSessionId, selectedFiles: Array.from(selectedZipFiles), defaultAuthorId: authorId || undefined, defaultAuthorName: authorName || undefined, defaultSeriesId: seriesId || undefined, defaultSeriesName: seriesName || undefined, defaultTags: tags ? tags.split(',').map(t => t.trim()).filter(t => t.length > 0) : undefined, createMissingAuthor, createMissingSeries, extractImages, }; // Add per-file metadata if any if (fileMetadata.size > 0) { const metadata: any = {}; fileMetadata.forEach((value, key) => { metadata[key] = value; }); requestBody.fileMetadata = metadata; } const response = await fetch('/api/stories/zip/import', { method: 'POST', headers: { 'Authorization': token ? `Bearer ${token}` : '', 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); const result: ZIPImportResponse = await response.json(); if (response.ok) { setZipImportResult(result); } else if (response.status === 401 || response.status === 403) { setError('Authentication required. Please log in.'); } else { setError(result.message || 'Failed to import files from ZIP'); } } catch (err) { setError('Error importing from ZIP: ' + (err as Error).message); } finally { setIsLoading(false); } }; const toggleZipFileSelection = (fileName: string) => { const newSelection = new Set(selectedZipFiles); if (newSelection.has(fileName)) { newSelection.delete(fileName); } else { newSelection.add(fileName); } setSelectedZipFiles(newSelection); }; const selectAllZipFiles = () => { if (zipAnalysis?.files) { const validFiles = zipAnalysis.files.filter(f => !f.error); setSelectedZipFiles(new Set(validFiles.map(f => f.fileName))); } }; const deselectAllZipFiles = () => { setSelectedZipFiles(new Set()); }; const resetForm = () => { setSelectedFile(null); setFileType(null); setValidationResult(null); setImportResult(null); setZipAnalysis(null); setZipSessionId(null); setSelectedZipFiles(new Set()); setFileMetadata(new Map()); setZipImportResult(null); setError(null); setAuthorName(''); setAuthorId(undefined); setSeriesName(''); setSeriesId(undefined); setSeriesVolume(''); setTags(''); }; const handleAuthorChange = (name: string, id?: string) => { setAuthorName(name); setAuthorId(id); }; const handleSeriesChange = (name: string, id?: string) => { setSeriesName(name); setSeriesId(id); }; // Show success screen for single file import if (importResult?.success) { return (

Import Completed

Your {importResult.fileType || fileType?.toUpperCase()} file has been successfully imported.

Story Title:

{importResult.storyTitle}

{importResult.wordCount && (
Word Count:

{importResult.wordCount.toLocaleString()} words

)} {importResult.extractedImages !== undefined && importResult.extractedImages > 0 && (
Extracted Images:

{importResult.extractedImages}

)} {importResult.warnings && importResult.warnings.length > 0 && (
Warnings:
    {importResult.warnings.map((warning, index) => (
  • {warning}
  • ))}
)}
); } // Show success screen for ZIP import if (zipImportResult) { return (

{zipImportResult.message}

{zipImportResult.successfulImports} of {zipImportResult.totalFiles} files imported successfully

Import Results

{zipImportResult.results.map((result, index) => (
{result.success ? ( ) : ( )}

{result.fileName || result.storyTitle || 'Unknown file'}

{result.success && result.storyTitle && (

Imported as: {result.storyTitle} {result.storyId && ( )}

)} {!result.success && (

{result.message}

)}
))}
); } return ( {error && (

{error}

)}
{/* File Upload */}

Select File

Choose an EPUB, PDF, or ZIP file from your device to import.

{selectedFile && (
{selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) {fileType && {fileType.toUpperCase()} }
)} {isValidating && (
Validating file...
)} {validationResult && fileType !== 'zip' && (
{validationResult.valid ? ( Valid {fileType?.toUpperCase()} ) : ( Invalid {fileType?.toUpperCase()} )}
)}
{/* ZIP File Selection */} {fileType === 'zip' && zipAnalysis?.success && zipAnalysis.files && (

Select Files to Import

{zipAnalysis.validFiles} valid files found in ZIP ({zipAnalysis.totalFiles} total)

{zipAnalysis.files.map((file, index) => (
{!file.error && ( toggleZipFileSelection(file.fileName)} className="mt-1" /> )}

{file.fileName}

{file.fileType} • {(file.fileSize / 1024).toFixed(2)} KB {file.extractedTitle && ` • ${file.extractedTitle}`}

{file.error && (

{file.error}

)}
))}
)} {/* Import Options - Show for all file types */} {fileType && (!zipAnalysis || (zipAnalysis && selectedZipFiles.size > 0)) && (

Import Options

Configure how the {fileType === 'zip' ? 'files' : 'file'} should be imported. {fileType === 'zip' && ' These settings apply to all selected files.'}

{seriesName && (
setSeriesVolume(e.target.value)} placeholder="Volume number in series" />
)}
setTags(e.target.value)} placeholder="Comma-separated tags (e.g., fantasy, adventure, romance)" />
{fileType === 'epub' && (
setPreserveReadingPosition(e.target.checked)} className="mr-2" />
)} {(fileType === 'pdf' || fileType === 'zip') && (
setExtractImages(e.target.checked)} className="mr-2" />
)}
setCreateMissingAuthor(e.target.checked)} className="mr-2" />
setCreateMissingSeries(e.target.checked)} className="mr-2" />
)} {/* Submit Button */} {fileType && fileType !== 'zip' && (
)} {fileType === 'zip' && zipAnalysis?.success && (
)}
); }