import React, { useState, useCallback, useRef, useEffect } from "react"; import { Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon, Notification, TextInput, FileInput, LoadingOverlay, Modal, Alert, Container, Stack, Group, Paper, SimpleGrid } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { useTranslation } from "react-i18next"; import UndoIcon from "@mui/icons-material/Undo"; import RedoIcon from "@mui/icons-material/Redo"; import AddIcon from "@mui/icons-material/Add"; import ContentCutIcon from "@mui/icons-material/ContentCut"; import DownloadIcon from "@mui/icons-material/Download"; import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; import DeleteIcon from "@mui/icons-material/Delete"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import ConstructionIcon from "@mui/icons-material/Construction"; import EventListIcon from "@mui/icons-material/EventList"; import DeselectIcon from "@mui/icons-material/Deselect"; import SelectAllIcon from "@mui/icons-material/SelectAll"; import { usePDFProcessor } from "../hooks/usePDFProcessor"; import { PDFDocument, PDFPage } from "../types/pageEditor"; import { fileStorage } from "../services/fileStorage"; import { generateThumbnailForFile } from "../utils/thumbnailUtils"; import { useUndoRedo } from "../hooks/useUndoRedo"; import { RotatePagesCommand, DeletePagesCommand, ReorderPageCommand, ToggleSplitCommand } from "../commands/pageCommands"; import { pdfExportService } from "../services/pdfExportService"; export interface PageEditorProps { file: { file: File; url: string } | null; setFile?: (file: { file: File; url: string } | null) => void; downloadUrl?: string | null; setDownloadUrl?: (url: string | null) => void; } const PageEditor: React.FC = ({ file, setFile, downloadUrl, setDownloadUrl, }) => { const { t } = useTranslation(); const { processPDFFile, loading: pdfLoading } = usePDFProcessor(); const [pdfDocument, setPdfDocument] = useState(null); const [selectedPages, setSelectedPages] = useState([]); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [csvInput, setCsvInput] = useState(""); const [showPageSelect, setShowPageSelect] = useState(false); const [filename, setFilename] = useState(""); const [draggedPage, setDraggedPage] = useState(null); const [dropTarget, setDropTarget] = useState(null); const [exportLoading, setExportLoading] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); const fileInputRef = useRef<() => void>(null); // Undo/Redo system const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo(); // Process uploaded file const handleFileUpload = useCallback(async (uploadedFile: File) => { if (!uploadedFile || uploadedFile.type !== 'application/pdf') { setError('Please upload a valid PDF file'); return; } setLoading(true); setError(null); try { const document = await processPDFFile(uploadedFile); setPdfDocument(document); setFilename(uploadedFile.name.replace(/\.pdf$/i, '')); setSelectedPages([]); if (document.pages.length > 0) { const thumbnail = await generateThumbnailForFile(uploadedFile); await fileStorage.storeFile(uploadedFile, thumbnail); } if (setFile) { const fileUrl = URL.createObjectURL(uploadedFile); setFile({ file: uploadedFile, url: fileUrl }); } setStatus(`PDF loaded successfully with ${document.totalPages} pages`); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF'; setError(errorMessage); console.error('PDF processing error:', err); } finally { setLoading(false); } }, [processPDFFile, setFile]); useEffect(() => { if (file?.file && !pdfDocument) { handleFileUpload(file.file); } }, [file, pdfDocument, handleFileUpload]); const selectAll = useCallback(() => { if (pdfDocument) { setSelectedPages(pdfDocument.pages.map(p => p.id)); } }, [pdfDocument]); const deselectAll = useCallback(() => setSelectedPages([]), []); const togglePage = useCallback((pageId: string) => { setSelectedPages(prev => prev.includes(pageId) ? prev.filter(id => id !== pageId) : [...prev, pageId] ); }, []); const parseCSVInput = useCallback((csv: string) => { if (!pdfDocument) return []; const pageIds: string[] = []; const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); ranges.forEach(range => { if (range.includes('-')) { const [start, end] = range.split('-').map(n => parseInt(n.trim())); for (let i = start; i <= end && i <= pdfDocument.totalPages; i++) { if (i > 0) { const page = pdfDocument.pages.find(p => p.pageNumber === i); if (page) pageIds.push(page.id); } } } else { const pageNum = parseInt(range); if (pageNum > 0 && pageNum <= pdfDocument.totalPages) { const page = pdfDocument.pages.find(p => p.pageNumber === pageNum); if (page) pageIds.push(page.id); } } }); return pageIds; }, [pdfDocument]); const updatePagesFromCSV = useCallback(() => { const pageIds = parseCSVInput(csvInput); setSelectedPages(pageIds); }, [csvInput, parseCSVInput]); const handleDragStart = useCallback((pageId: string) => { setDraggedPage(pageId); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); if (!draggedPage) return; // Get the element under the mouse cursor const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); if (!elementUnderCursor) return; // Find the closest page container const pageContainer = elementUnderCursor.closest('[data-page-id]'); if (pageContainer) { const pageId = pageContainer.getAttribute('data-page-id'); if (pageId && pageId !== draggedPage) { setDropTarget(pageId); return; } } // Check if over the end zone const endZone = elementUnderCursor.closest('[data-drop-zone="end"]'); if (endZone) { setDropTarget('end'); return; } // If not over any valid drop target, clear it setDropTarget(null); }, [draggedPage]); const handleDragEnter = useCallback((pageId: string) => { if (draggedPage && pageId !== draggedPage) { setDropTarget(pageId); } }, [draggedPage]); const handleDragLeave = useCallback(() => { // Don't clear drop target on drag leave - let dragover handle it }, []); const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => { e.preventDefault(); if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return; let targetIndex: number; if (targetPageId === 'end') { targetIndex = pdfDocument.pages.length; } else { targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId); if (targetIndex === -1) return; } const command = new ReorderPageCommand( pdfDocument, setPdfDocument, draggedPage, targetIndex ); executeCommand(command); setDraggedPage(null); setDropTarget(null); setStatus('Page reordered'); }, [draggedPage, pdfDocument, executeCommand]); const handleEndZoneDragEnter = useCallback(() => { if (draggedPage) { setDropTarget('end'); } }, [draggedPage]); const handleRotate = useCallback((direction: 'left' | 'right') => { if (!pdfDocument || selectedPages.length === 0) return; const rotation = direction === 'left' ? -90 : 90; const command = new RotatePagesCommand( pdfDocument, setPdfDocument, selectedPages, rotation ); executeCommand(command); setStatus(`Rotated ${selectedPages.length} pages ${direction}`); }, [pdfDocument, selectedPages, executeCommand]); const handleDelete = useCallback(() => { if (!pdfDocument || selectedPages.length === 0) return; const command = new DeletePagesCommand( pdfDocument, setPdfDocument, selectedPages ); executeCommand(command); setSelectedPages([]); setStatus(`Deleted ${selectedPages.length} pages`); }, [pdfDocument, selectedPages, executeCommand]); const handleSplit = useCallback(() => { if (!pdfDocument || selectedPages.length === 0) return; const command = new ToggleSplitCommand( pdfDocument, setPdfDocument, selectedPages ); executeCommand(command); setStatus(`Split markers toggled for ${selectedPages.length} pages`); }, [pdfDocument, selectedPages, executeCommand]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { if (!pdfDocument) return; const exportPageIds = selectedOnly ? selectedPages : []; const preview = pdfExportService.getExportInfo(pdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); }, [pdfDocument, selectedPages]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { if (!pdfDocument) return; setExportLoading(true); try { const exportPageIds = selectedOnly ? selectedPages : []; const errors = pdfExportService.validateExport(pdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { setError(errors.join(', ')); return; } const hasSplitMarkers = pdfDocument.pages.some(page => page.splitBefore); if (hasSplitMarkers) { const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { selectedOnly, filename, splitDocuments: true }) as { blobs: Blob[]; filenames: string[] }; result.blobs.forEach((blob, index) => { setTimeout(() => { pdfExportService.downloadFile(blob, result.filenames[index]); }, index * 500); }); setStatus(`Exported ${result.blobs.length} split documents`); } else { const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { selectedOnly, filename }) as { blob: Blob; filename: string }; pdfExportService.downloadFile(result.blob, result.filename); setStatus('PDF exported successfully'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Export failed'; setError(errorMessage); } finally { setExportLoading(false); } }, [pdfDocument, selectedPages, filename]); const handleUndo = useCallback(() => { if (undo()) { setStatus('Operation undone'); } }, [undo]); const handleRedo = useCallback(() => { if (redo()) { setStatus('Operation redone'); } }, [redo]); if (!pdfDocument) { return ( {error && ( setError(null)}> {error} )} files[0] && handleFileUpload(files[0])} accept={["application/pdf"]} multiple={false} h="60vh" style={{ minHeight: 400 }} >
Drop a PDF file here or click to upload Supports PDF files only
); } return ( setFilename(e.target.value)} placeholder="Enter filename" style={{ minWidth: 200 }} /> {showPageSelect && ( setCsvInput(e.target.value)} placeholder="1,3,5-10" label="Page Selection" onBlur={updatePagesFromCSV} onKeyDown={(e) => e.key === 'Enter' && updatePagesFromCSV()} style={{ flex: 1 }} /> {selectedPages.length > 0 && ( Selected: {selectedPages.length} pages )} )} handleRotate('left')} disabled={selectedPages.length === 0}> handleRotate('right')} disabled={selectedPages.length === 0}>
{pdfDocument.pages.map((page, index) => ( {page.splitBefore && index > 0 && (
)}
{ if (!draggedPage || page.id === draggedPage) return 'translateX(0)'; if (dropTarget === page.id) { return 'translateX(20px)'; // Move slightly right to indicate drop position } return 'translateX(0)'; })(), transition: 'transform 0.2s ease-in-out' }} draggable onDragStart={() => handleDragStart(page.id)} onDragOver={handleDragOver} onDragEnter={() => handleDragEnter(page.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, page.id)} >
{`Page {/* Page number overlay - shows on hover */} {page.pageNumber} {/* Hover controls */}
{ e.stopPropagation(); const command = new RotatePagesCommand( pdfDocument, setPdfDocument, [page.id], -90 ); executeCommand(command); setStatus(`Rotated page ${page.pageNumber} left`); }} > { e.stopPropagation(); const command = new RotatePagesCommand( pdfDocument, setPdfDocument, [page.id], 90 ); executeCommand(command); setStatus(`Rotated page ${page.pageNumber} right`); }} > { e.stopPropagation(); const command = new DeletePagesCommand( pdfDocument, setPdfDocument, [page.id] ); executeCommand(command); setStatus(`Deleted page ${page.pageNumber}`); }} > { e.stopPropagation(); const command = new ToggleSplitCommand( pdfDocument, setPdfDocument, [page.id] ); executeCommand(command); setStatus(`Split marker toggled for page ${page.pageNumber}`); }} > togglePage(page.id)} styles={{ input: { backgroundColor: 'white' } }} />
))} {/* Landing zone at the end */}
handleDrop(e, 'end')} > Drop here to
move to end
setShowExportModal(false)} title="Export Preview" > {exportPreview && ( Pages to export: {exportPreview.pageCount} {exportPreview.splitCount > 1 && ( Split into documents: {exportPreview.splitCount} )} Estimated size: {exportPreview.estimatedSize} {pdfDocument && pdfDocument.pages.some(p => p.splitBefore) && ( This will create multiple PDF files based on split markers. )} )} file && handleFileUpload(file)} style={{ display: 'none' }} /> {status && ( setStatus(null)} style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }} > {status} )}
); }; export default PageEditor;