Page editor updates

This commit is contained in:
Reece 2025-06-16 19:57:50 +01:00
parent 7fc850b138
commit e7995a0256

View File

@ -20,6 +20,8 @@ import ConstructionIcon from "@mui/icons-material/Construction";
import EventListIcon from "@mui/icons-material/EventList"; import EventListIcon from "@mui/icons-material/EventList";
import DeselectIcon from "@mui/icons-material/Deselect"; import DeselectIcon from "@mui/icons-material/Deselect";
import SelectAllIcon from "@mui/icons-material/SelectAll"; import SelectAllIcon from "@mui/icons-material/SelectAll";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import { usePDFProcessor } from "../hooks/usePDFProcessor"; import { usePDFProcessor } from "../hooks/usePDFProcessor";
import { PDFDocument, PDFPage } from "../types/pageEditor"; import { PDFDocument, PDFPage } from "../types/pageEditor";
import { fileStorage } from "../services/fileStorage"; import { fileStorage } from "../services/fileStorage";
@ -62,6 +64,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
const [exportLoading, setExportLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false);
const [showExportModal, setShowExportModal] = useState(false); const [showExportModal, setShowExportModal] = useState(false);
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
const [movingPage, setMovingPage] = useState<string | null>(null);
const fileInputRef = useRef<() => void>(null); const fileInputRef = useRef<() => void>(null);
// Undo/Redo system // Undo/Redo system
@ -163,13 +166,13 @@ const PageEditor: React.FC<PageEditorProps> = ({
const handleDragOver = useCallback((e: React.DragEvent) => { const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
if (!draggedPage) return; if (!draggedPage) return;
// Get the element under the mouse cursor // Get the element under the mouse cursor
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
if (!elementUnderCursor) return; if (!elementUnderCursor) return;
// Find the closest page container // Find the closest page container
const pageContainer = elementUnderCursor.closest('[data-page-id]'); const pageContainer = elementUnderCursor.closest('[data-page-id]');
if (pageContainer) { if (pageContainer) {
@ -179,14 +182,14 @@ const PageEditor: React.FC<PageEditorProps> = ({
return; return;
} }
} }
// Check if over the end zone // Check if over the end zone
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]'); const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
if (endZone) { if (endZone) {
setDropTarget('end'); setDropTarget('end');
return; return;
} }
// If not over any valid drop target, clear it // If not over any valid drop target, clear it
setDropTarget(null); setDropTarget(null);
}, [draggedPage]); }, [draggedPage]);
@ -345,33 +348,28 @@ const PageEditor: React.FC<PageEditorProps> = ({
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}> <Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={loading || pdfLoading} /> <LoadingOverlay visible={loading || pdfLoading} />
<Box p="xl"> <Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{error && (
<Alert color="red" mb="md" onClose={() => setError(null)}>
{error}
</Alert>
)}
<Dropzone <Dropzone
onDrop={(files) => files[0] && handleFileUpload(files[0])} onDrop={(files) => files[0] && handleFileUpload(files[0])}
accept={["application/pdf"]} accept={["application/pdf"]}
multiple={false} multiple={false}
h="60vh" h="60vh"
style={{ minHeight: 400 }} style={{ minHeight: 400 }}
> >
<Center h="100%"> <Center h="100%">
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<UploadFileIcon style={{ fontSize: 64 }} /> <UploadFileIcon style={{ fontSize: 64 }} />
<Text size="xl" fw={500}> <Text size="xl" fw={500}>
Drop a PDF file here or click to upload Drop a PDF file here or click to upload
</Text> </Text>
<Text size="md" c="dimmed"> <Text size="md" c="dimmed">
Supports PDF files only Supports PDF files only
</Text> </Text>
</Stack> </Stack>
</Center> </Center>
</Dropzone> </Dropzone>
</Box> </Container>
</Box> </Box>
); );
} }
@ -392,6 +390,14 @@ const PageEditor: React.FC<PageEditorProps> = ({
.page-container:hover { .page-container:hover {
transform: scale(1.02); transform: scale(1.02);
} }
.page-move-animation {
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.page-moving {
z-index: 10;
transform: scale(1.05);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%, 100% {
opacity: 1; opacity: 1;
@ -489,30 +495,15 @@ const PageEditor: React.FC<PageEditorProps> = ({
{page.splitBefore && index > 0 && ( {page.splitBefore && index > 0 && (
<div <div
style={{ style={{
width: '4px', width: '2px',
height: '15rem', height: '20rem',
border: '2px dashed #3b82f6', borderLeft: '2px dashed #3b82f6',
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderRadius: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '-0.75rem', marginLeft: '-0.75rem',
marginRight: '-0.75rem', marginRight: '-0.75rem',
position: 'relative',
flexShrink: 0 flexShrink: 0
}} }}
> />
<ContentCutIcon
style={{
fontSize: 18,
color: '#3b82f6',
backgroundColor: 'white',
borderRadius: '50%',
padding: '3px'
}}
/>
</div>
)} )}
<div <div
data-page-id={page.id} data-page-id={page.id}
@ -520,23 +511,25 @@ const PageEditor: React.FC<PageEditorProps> = ({
!rounded-lg !rounded-lg
cursor-grab cursor-grab
select-none select-none
w-[15rem] w-[20rem]
h-[15rem] h-[20rem]
flex items-center justify-center flex items-center justify-center
flex-shrink-0 flex-shrink-0
shadow-sm shadow-sm
hover:shadow-md hover:shadow-md
transition-all transition-all
relative relative
page-move-animation
${selectedPages.includes(page.id) ${selectedPages.includes(page.id)
? 'ring-2 ring-blue-500 bg-blue-50' ? 'ring-2 ring-blue-500 bg-blue-50'
: 'bg-white hover:bg-gray-50'} : 'bg-white hover:bg-gray-50'}
${draggedPage === page.id ? 'opacity-50 scale-95' : ''} ${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
${movingPage === page.id ? 'page-moving' : ''}
`} `}
style={{ style={{
transform: (() => { transform: (() => {
if (!draggedPage || page.id === draggedPage) return 'translateX(0)'; if (!draggedPage || page.id === draggedPage) return 'translateX(0)';
if (dropTarget === page.id) { if (dropTarget === page.id) {
return 'translateX(20px)'; // Move slightly right to indicate drop position return 'translateX(20px)'; // Move slightly right to indicate drop position
} }
@ -606,6 +599,62 @@ const PageEditor: React.FC<PageEditorProps> = ({
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
> >
<Tooltip label="Move Left">
<ActionIcon
size="md"
variant="subtle"
c="white"
disabled={index === 0}
onClick={(e) => {
e.stopPropagation();
if (index > 0 && !movingPage) {
setMovingPage(page.id);
setTimeout(() => {
const command = new ReorderPageCommand(
pdfDocument,
setPdfDocument,
page.id,
index - 1
);
executeCommand(command);
setTimeout(() => setMovingPage(null), 100);
setStatus(`Moved page ${page.pageNumber} left`);
}, 50);
}
}}
>
<ArrowBackIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
<Tooltip label="Move Right">
<ActionIcon
size="md"
variant="subtle"
c="white"
disabled={index === pdfDocument.pages.length - 1}
onClick={(e) => {
e.stopPropagation();
if (index < pdfDocument.pages.length - 1 && !movingPage) {
setMovingPage(page.id);
setTimeout(() => {
const command = new ReorderPageCommand(
pdfDocument,
setPdfDocument,
page.id,
index + 1
);
executeCommand(command);
setTimeout(() => setMovingPage(null), 100);
setStatus(`Moved page ${page.pageNumber} right`);
}, 50);
}
}}
>
<ArrowForwardIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
<Tooltip label="Rotate Left"> <Tooltip label="Rotate Left">
<ActionIcon <ActionIcon
size="md" size="md"
@ -668,25 +717,27 @@ const PageEditor: React.FC<PageEditorProps> = ({
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Split Here"> {index > 0 && (
<ActionIcon <Tooltip label="Split Here">
size="md" <ActionIcon
variant="subtle" size="md"
c="white" variant="subtle"
onClick={(e) => { c="white"
e.stopPropagation(); onClick={(e) => {
const command = new ToggleSplitCommand( e.stopPropagation();
pdfDocument, const command = new ToggleSplitCommand(
setPdfDocument, pdfDocument,
[page.id] setPdfDocument,
); [page.id]
executeCommand(command); );
setStatus(`Split marker toggled for page ${page.pageNumber}`); executeCommand(command);
}} setStatus(`Split marker toggled for page ${page.pageNumber}`);
> }}
<ContentCutIcon style={{ fontSize: 20 }} /> >
</ActionIcon> <ContentCutIcon style={{ fontSize: 20 }} />
</Tooltip> </ActionIcon>
</Tooltip>
)}
<Tooltip label="Select Page"> <Tooltip label="Select Page">
<Checkbox <Checkbox
@ -714,31 +765,24 @@ const PageEditor: React.FC<PageEditorProps> = ({
</div> </div>
</React.Fragment> </React.Fragment>
))} ))}
{/* Landing zone at the end */} {/* Landing zone at the end */}
<div <div className="w-[20rem] h-[20rem] flex items-center justify-center flex-shrink-0">
data-drop-zone="end" <div
style={{ data-drop-zone="end"
width: '15rem', className={`cursor-pointer select-none w-[15rem] h-[15rem] flex items-center justify-center flex-shrink-0 shadow-sm hover:shadow-md transition-all relative ${dropTarget === 'end' ? 'ring-2 ring-green-500 bg-green-50' : 'bg-white hover:bg-blue-50 border-2 border-dashed border-gray-300 hover:border-blue-400'}`}
height: '15rem', style={{
border: '2px dashed #9ca3af', borderRadius: '12px'
borderRadius: '8px', }}
display: 'flex', onDragOver={handleDragOver}
alignItems: 'center', onDragEnter={handleEndZoneDragEnter}
justifyContent: 'center', onDragLeave={handleDragLeave}
flexShrink: 0, onDrop={(e) => handleDrop(e, 'end')}
backgroundColor: dropTarget === 'end' ? '#ecfdf5' : 'transparent', >
borderColor: dropTarget === 'end' ? '#10b981' : '#9ca3af', <Text c="dimmed" size="sm" ta="center" fw={500}>
transition: 'all 0.2s ease-in-out' Drop here to<br />move to end
}} </Text>
onDragOver={handleDragOver} </div>
onDragEnter={handleEndZoneDragEnter}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, 'end')}
>
<Text c="dimmed" size="sm" ta="center">
Drop here to<br />move to end
</Text>
</div> </div>
</div> </div>