{
+ if (el) {
+ fileRefs.current.set(file.id, el);
+ } else {
+ fileRefs.current.delete(file.id);
+ }
+ }}
+ data-file-id={file.id}
+ className={`
+ ${styles.pageContainer}
+ !rounded-lg
+ cursor-grab
+ select-none
+ w-[20rem]
+ h-[24rem]
+ flex flex-col items-center justify-center
+ flex-shrink-0
+ shadow-sm
+ hover:shadow-md
+ transition-all
+ relative
+ ${selectionMode
+ ? 'bg-white hover:bg-gray-50'
+ : 'bg-white hover:bg-gray-50'}
+ ${draggedFile === file.id ? 'opacity-50 scale-95' : ''}
+ `}
+ style={{
+ transform: (() => {
+ if (!isAnimating && draggedFile && file.id !== draggedFile && dropTarget === file.id) {
+ return 'translateX(20px)';
+ }
+ return 'translateX(0)';
+ })(),
+ transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
+ }}
+ draggable
+ onDragStart={() => onDragStart(file.id)}
+ onDragEnd={onDragEnd}
+ onDragOver={onDragOver}
+ onDragEnter={() => onDragEnter(file.id)}
+ onDragLeave={onDragLeave}
+ onDrop={(e) => onDrop(e, file.id)}
+ >
+ {selectionMode && (
+
e.stopPropagation()}
+ onDragStart={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ {
+ event.stopPropagation();
+ onToggleFile(file.id);
+ }}
+ onClick={(e) => e.stopPropagation()}
+ size="sm"
+ />
+
+ )}
+
+ {/* File content area */}
+
+ {/* Stacked file effect - multiple shadows to simulate pages */}
+
+

+
+
+ {/* Page count badge */}
+
+ {file.pageCount} pages
+
+
+ {/* File name overlay */}
+
+ {file.name}
+
+
+ {/* Hover controls */}
+
+
+ {
+ e.stopPropagation();
+ onViewFile(file.id);
+ onSetStatus(`Opened ${file.name}`);
+ }}
+ >
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onMergeFromHere(file.id);
+ onSetStatus(`Starting merge from ${file.name}`);
+ }}
+ >
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onSplitFile(file.id);
+ onSetStatus(`Opening ${file.name} in page editor`);
+ }}
+ >
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onDeleteFile(file.id);
+ onSetStatus(`Deleted ${file.name}`);
+ }}
+ >
+
+
+
+
+
+
+
+
+ {/* File info */}
+
+
+ {file.name}
+
+
+ {formatFileSize(file.size)}
+
+
+
+ );
+};
+
+export default FileThumbnail;
\ No newline at end of file
diff --git a/frontend/src/components/editor/PageEditor.module.css b/frontend/src/components/editor/PageEditor.module.css
new file mode 100644
index 000000000..5901e80e6
--- /dev/null
+++ b/frontend/src/components/editor/PageEditor.module.css
@@ -0,0 +1,63 @@
+/* Page container hover effects */
+.pageContainer {
+ transition: transform 0.2s ease-in-out;
+}
+
+.pageContainer:hover {
+ transform: scale(1.02);
+}
+
+.pageContainer:hover .pageNumber {
+ opacity: 1 !important;
+}
+
+.pageContainer:hover .pageHoverControls {
+ opacity: 1 !important;
+}
+
+/* Checkbox container - prevent transform inheritance */
+.checkboxContainer {
+ transform: none !important;
+ transition: none !important;
+}
+
+/* Page movement animations */
+.pageMoveAnimation {
+ transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+}
+
+.pageMoving {
+ z-index: 10;
+ transform: scale(1.05);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
+}
+
+/* Multi-page drag indicator */
+.multiDragIndicator {
+ position: fixed;
+ background: rgba(59, 130, 246, 0.9);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ pointer-events: none;
+ z-index: 1000;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ transform: translate(-50%, -50%);
+ backdrop-filter: blur(4px);
+}
+
+/* Animations */
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.pulse {
+ animation: pulse 1s infinite;
+}
\ No newline at end of file
diff --git a/frontend/src/components/editor/PageEditor.tsx b/frontend/src/components/editor/PageEditor.tsx
index b324c08af..0897266f5 100644
--- a/frontend/src/components/editor/PageEditor.tsx
+++ b/frontend/src/components/editor/PageEditor.tsx
@@ -4,25 +4,8 @@ import {
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 ArrowBackIcon from "@mui/icons-material/ArrowBack";
-import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
-import CloseIcon from "@mui/icons-material/Close";
import { usePDFProcessor } from "../../hooks/usePDFProcessor";
import { PDFDocument, PDFPage } from "../../types/pageEditor";
import { fileStorage } from "../../services/fileStorage";
@@ -36,13 +19,19 @@ import {
ToggleSplitCommand
} from "../../commands/pageCommands";
import { pdfExportService } from "../../services/pdfExportService";
+import styles from './PageEditor.module.css';
+import PageThumbnail from './PageThumbnail';
+import BulkSelectionPanel from './BulkSelectionPanel';
+import DragDropGrid from './shared/DragDropGrid';
+import FilePickerModal from '../shared/FilePickerModal';
+import FileUploadSelector from '../shared/FileUploadSelector';
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;
-
+
// Optional callbacks to expose internal functions
onFunctionsReady?: (functions: {
handleUndo: () => void;
@@ -66,6 +55,7 @@ const PageEditor = ({
downloadUrl,
setDownloadUrl,
onFunctionsReady,
+ sharedFiles,
}: PageEditorProps) => {
const { t } = useTranslation();
const { processPDFFile, loading: pdfLoading } = usePDFProcessor();
@@ -95,8 +85,38 @@ const PageEditor = ({
const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo();
// Process uploaded file
- const handleFileUpload = useCallback(async (uploadedFile: File) => {
- if (!uploadedFile || uploadedFile.type !== 'application/pdf') {
+ const handleFileUpload = useCallback(async (uploadedFile: File | any) => {
+ if (!uploadedFile) {
+ setError('No file provided');
+ return;
+ }
+
+ let fileToProcess: File;
+
+ // Handle FileWithUrl objects from storage
+ if (uploadedFile.storedInIndexedDB && uploadedFile.arrayBuffer) {
+ try {
+ console.log('Converting FileWithUrl to File:', uploadedFile.name);
+ const arrayBuffer = await uploadedFile.arrayBuffer();
+ const blob = new Blob([arrayBuffer], { type: uploadedFile.type || 'application/pdf' });
+ fileToProcess = new File([blob], uploadedFile.name, {
+ type: uploadedFile.type || 'application/pdf',
+ lastModified: uploadedFile.lastModified || Date.now()
+ });
+ } catch (error) {
+ console.error('Error converting FileWithUrl:', error);
+ setError('Unable to load file from storage');
+ return;
+ }
+ } else if (uploadedFile instanceof File) {
+ fileToProcess = uploadedFile;
+ } else {
+ setError('Invalid file object');
+ console.error('handleFileUpload received unsupported object:', uploadedFile);
+ return;
+ }
+
+ if (fileToProcess.type !== 'application/pdf') {
setError('Please upload a valid PDF file');
return;
}
@@ -105,19 +125,22 @@ const PageEditor = ({
setError(null);
try {
- const document = await processPDFFile(uploadedFile);
+ const document = await processPDFFile(fileToProcess);
setPdfDocument(document);
- setFilename(uploadedFile.name.replace(/\.pdf$/i, ''));
+ setFilename(fileToProcess.name.replace(/\.pdf$/i, ''));
setSelectedPages([]);
if (document.pages.length > 0) {
- const thumbnail = await generateThumbnailForFile(uploadedFile);
- await fileStorage.storeFile(uploadedFile, thumbnail);
+ // Only store if it's a new file (not from storage)
+ if (!uploadedFile.storedInIndexedDB) {
+ const thumbnail = await generateThumbnailForFile(fileToProcess);
+ await fileStorage.storeFile(fileToProcess, thumbnail);
+ }
}
if (setFile) {
- const fileUrl = URL.createObjectURL(uploadedFile);
- setFile({ file: uploadedFile, url: fileUrl });
+ const fileUrl = URL.createObjectURL(fileToProcess);
+ setFile({ file: fileToProcess, url: fileUrl });
}
setStatus(`PDF loaded successfully with ${document.totalPages} pages`);
@@ -562,18 +585,18 @@ const PageEditor = ({
});
}
}, [
- onFunctionsReady,
- handleUndo,
- handleRedo,
- canUndo,
- canRedo,
- handleRotate,
- handleDelete,
- handleSplit,
- showExportPreview,
- exportLoading,
- selectionMode,
- selectedPages,
+ onFunctionsReady,
+ handleUndo,
+ handleRedo,
+ canUndo,
+ canRedo,
+ handleRotate,
+ handleDelete,
+ handleSplit,
+ showExportPreview,
+ exportLoading,
+ selectionMode,
+ selectedPages,
closePdf
]);
@@ -583,26 +606,15 @@ const PageEditor = ({