refactoring, linting, formatting

This commit is contained in:
2026-05-17 02:05:27 +02:00
parent bdbb6c0a1c
commit 07f4361573
38 changed files with 6121 additions and 2647 deletions

View File

@@ -1,5 +1,8 @@
import React, { useEffect, useState, useRef } from 'react';
import type { PageRef } from '../pdf/pdfTypes';
import React, { useRef, useState } from "react";
import type { PageRef } from "../pdf/pdfTypes";
import CopyPagesDialog from "./PageWorkspace/CopyPagesDialog";
import PageGrid from "./PageWorkspace/PageGrid";
import PageSelectionToolbar from "./PageWorkspace/PageSelectionToolbar";
interface ReorderPanelProps {
pages: PageRef[];
@@ -17,7 +20,7 @@ interface ReorderPanelProps {
onToggleSelect: (
pageId: string,
visualIndex: number,
e: React.MouseEvent<HTMLButtonElement>
e: React.MouseEvent<HTMLButtonElement>,
) => void;
onSelectAll: () => void;
@@ -48,13 +51,11 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
const [isCopyDragging, setIsCopyDragging] = useState(false);
const [copyDialogOpen, setCopyDialogOpen] = useState(false);
const [copyTargetPosition, setCopyTargetPosition] = useState('');
const [copyTargetPosition, setCopyTargetPosition] = useState("");
const [copyDialogError, setCopyDialogError] = useState<string | null>(null);
const dragGhostRef = useRef<HTMLDivElement | null>(null);
const isSelected = (pageId: string) => selectedPageIds.includes(pageId);
const cleanupDragGhost = () => {
if (dragGhostRef.current && dragGhostRef.current.parentNode) {
dragGhostRef.current.parentNode.removeChild(dragGhostRef.current);
@@ -71,7 +72,7 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
if (!draggedPage) return [];
const selectedInVisualOrder = pages.filter((page) =>
selectedPageIds.includes(page.id)
selectedPageIds.includes(page.id),
);
const draggingIsSelected =
@@ -84,20 +85,20 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
const createDragGhost = (e: React.DragEvent, count: number) => {
cleanupDragGhost();
const ghost = document.createElement('div');
ghost.textContent = count === 1 ? '1 page' : `${count} pages`;
const ghost = document.createElement("div");
ghost.textContent = count === 1 ? "1 page" : `${count} pages`;
ghost.style.position = 'fixed';
ghost.style.top = '0';
ghost.style.left = '0';
ghost.style.padding = '4px 8px';
ghost.style.borderRadius = '999px';
ghost.style.background = '#111827';
ghost.style.color = '#e5e7eb';
ghost.style.fontSize = '12px';
ghost.style.position = "fixed";
ghost.style.top = "0";
ghost.style.left = "0";
ghost.style.padding = "4px 8px";
ghost.style.borderRadius = "999px";
ghost.style.background = "#111827";
ghost.style.color = "#e5e7eb";
ghost.style.fontSize = "12px";
ghost.style.fontFamily =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
ghost.style.zIndex = '9999';
ghost.style.zIndex = "9999";
document.body.appendChild(ghost);
dragGhostRef.current = ghost;
@@ -106,6 +107,13 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
e.dataTransfer.setDragImage(ghost, rect.width / 2, rect.height / 2);
};
const resetDragState = () => {
cleanupDragGhost();
setDraggingIndex(null);
setDropIndex(null);
setIsCopyDragging(false);
};
const handleDragStart = (visualIndex: number) => (e: React.DragEvent) => {
setDraggingIndex(visualIndex);
setDropIndex(visualIndex);
@@ -113,19 +121,16 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
const copying = isCopyModifierPressed(e);
setIsCopyDragging(copying);
e.dataTransfer.effectAllowed = 'copyMove';
e.dataTransfer.dropEffect = copying ? 'copy' : 'move';
e.dataTransfer.setData('text/plain', String(visualIndex));
e.dataTransfer.effectAllowed = "copyMove";
e.dataTransfer.dropEffect = copying ? "copy" : "move";
e.dataTransfer.setData("text/plain", String(visualIndex));
const draggedPages = getDraggedPages(visualIndex);
createDragGhost(e, draggedPages.length);
};
const handleDragEnd = () => {
cleanupDragGhost();
setDraggingIndex(null);
setDropIndex(null);
setIsCopyDragging(false);
resetDragState();
};
const handleCardDragOver = (visualIndex: number) => (e: React.DragEvent) => {
@@ -136,7 +141,7 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
const copying = isCopyModifierPressed(e);
setIsCopyDragging(copying);
e.dataTransfer.dropEffect = copying ? 'copy' : 'move';
e.dataTransfer.dropEffect = copying ? "copy" : "move";
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - rect.left;
@@ -153,7 +158,7 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
const copying = isCopyModifierPressed(e);
setIsCopyDragging(copying);
e.dataTransfer.dropEffect = copying ? 'copy' : 'move';
e.dataTransfer.dropEffect = copying ? "copy" : "move";
setDropIndex(pages.length);
};
@@ -172,7 +177,7 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
if (shouldCopy) {
onCopyPagesToSlot(
draggedPages.map((page) => page.id),
dropIndex
dropIndex,
);
setDraggingIndex(null);
@@ -207,35 +212,23 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
setIsCopyDragging(false);
};
const handleDeleteClick = (pageId: string) => () => {
const handleDeleteClick = (pageId: string) => {
onDelete(pageId);
setDraggingIndex(null);
setDropIndex(null);
};
const handleRotateClickClockwise = (pageId: string) => () => {
onRotateClockwise(pageId);
};
const handleRotateClickCounterclockwise = (pageId: string) => () => {
onRotateCounterclockwise(pageId);
};
const handleCardClick = (pageId: string) => () => {
onOpenPreview(pageId);
};
const handleCheckboxClick =
(pageId: string, visualIndex: number) =>
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); // don't trigger preview
onToggleSelect(pageId, visualIndex, e);
};
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onToggleSelect(pageId, visualIndex, e);
};
const handleCopySelectedClick = () => {
if (selectedPageIds.length === 0) return;
setCopyTargetPosition(String(pages.length + 1)); // default: after last page
setCopyTargetPosition(String(pages.length + 1));
setCopyDialogError(null);
setCopyDialogOpen(true);
};
@@ -245,11 +238,16 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
setCopyDialogError(null);
};
const handleCopyTargetPositionChange = (value: string) => {
setCopyTargetPosition(value);
setCopyDialogError(null);
};
const handleCopyDialogConfirm = (e?: React.FormEvent) => {
e?.preventDefault();
if (selectedPageIds.length === 0) {
setCopyDialogError('No pages selected.');
setCopyDialogError("No pages selected.");
return;
}
@@ -267,23 +265,6 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
setCopyDialogError(null);
};
useEffect(() => {
if (!copyDialogOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
handleCopyDialogCancel();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [copyDialogOpen]);
if (!hasPdf) {
return (
<div className="card">
@@ -293,554 +274,66 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
);
}
const showLeftLine = (visualIndex: number) =>
dropIndex !== null && dropIndex === visualIndex && draggingIndex !== null;
const showRightLine = (visualIndex: number) =>
dropIndex !== null &&
dropIndex === visualIndex + 1 &&
draggingIndex !== null;
const showEndLine = () =>
dropIndex !== null && dropIndex === pages.length && draggingIndex !== null;
// For highlighting the whole selection while dragging it
const draggingPage = draggingIndex != null ? pages[draggingIndex] : null;
const draggingSelectionActive =
draggingPage != null &&
selectedPageIds.length > 0 &&
selectedPageIds.includes(draggingPage.id);
const dropIndicatorColor = isCopyDragging ? '#16a34a' : '#2563eb';
const dropIndicatorColor = isCopyDragging ? "#16a34a" : "#2563eb";
return (
<>
<div className="card">
<h2>Pages</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}>
<p style={{ fontSize: "0.85rem", color: "#6b7280" }}>
Tap/click a page to preview it. Use the checkbox to select pages
(Shift for ranges). Drag to reorder; dragging a selected page moves the
whole selection. Hold Ctrl/ while dropping to copy instead of move.
Shortcuts: Ctrl/+A selects all, Delete removes selected pages, Esc clears
selection.
(Shift for ranges). Drag to reorder; dragging a selected page moves
the whole selection. Hold Ctrl/ while dropping to copy instead of
move. Shortcuts: Ctrl/+A selects all, Delete removes selected pages,
Esc clears selection.
</p>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '0.5rem',
fontSize: '0.85rem',
}}
>
<span>
Selected: <strong>{selectedPageIds.length}</strong>
</span>
<div
style={{
display: 'flex',
gap: '0.4rem',
}}
>
{selectedPageIds.length > 0 && (
<button
type="button"
onClick={handleCopySelectedClick}
disabled={selectedPageIds.length === 0}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.6rem',
fontSize: '0.8rem',
background: '#dcfce7',
color: '#166534',
cursor: 'pointer',
}}
title={'Copy selected pages to another position'}
>
Copy selected
</button>
)}
{selectedPageIds.length > 0 && (
<button
type="button"
onClick={onDeleteSelected}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.6rem',
fontSize: '0.8rem',
background: '#fee2e2',
color: '#b91c1c',
cursor: 'pointer',
}}
>
Delete selected
</button>
)}
<button
type="button"
onClick={onSelectAll}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.6rem',
fontSize: '0.8rem',
background: '#8dcd8d',
color: '#111827',
cursor: 'pointer',
}}
>
Select all
</button>
<button
type="button"
onClick={onClearSelection}
disabled={selectedPageIds.length === 0}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.6rem',
fontSize: '0.8rem',
background: '#e5e7eb',
color: selectedPageIds.length === 0 ? '#6b7280' : '#111827',
cursor: selectedPageIds.length === 0 ? 'default' : 'pointer',
}}
>
Clear selection
</button>
</div>
</div>
<PageSelectionToolbar
selectedCount={selectedPageIds.length}
onCopySelected={handleCopySelectedClick}
onDeleteSelected={onDeleteSelected}
onSelectAll={onSelectAll}
onClearSelection={onClearSelection}
/>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
alignItems: 'flex-start',
marginBottom: '0.75rem',
}}
<PageGrid
pages={pages}
thumbnails={thumbnails}
selectedPageIds={selectedPageIds}
isBusy={isBusy}
draggingIndex={draggingIndex}
dropIndex={dropIndex}
draggingSelectionActive={draggingSelectionActive}
isCopyDragging={isCopyDragging}
dropIndicatorColor={dropIndicatorColor}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onCardDragOver={handleCardDragOver}
onEndSlotDragOver={handleEndSlotDragOver}
onDrop={handleDrop}
>
{pages.map((page, visualIndex) => {
const thumb = thumbnails[page.id];
const rotation = page.rotation;
const selected = isSelected(page.id);
const isDraggingCard =
draggingIndex != null &&
((draggingSelectionActive && selected) ||
(!draggingSelectionActive && visualIndex === draggingIndex));
return (
<div
key={page.id}
draggable
onDragStart={handleDragStart(visualIndex)}
onDragEnd={handleDragEnd}
onDragOver={handleCardDragOver(visualIndex)}
onClick={handleCardClick(page.id)}
style={{
position: 'relative',
width: '162px',
padding: '0.4rem',
borderRadius: '0.5rem',
border: '1px solid #e5e7eb',
background: isDraggingCard
? isCopyDragging
? '#dcfce7'
: '#dbeafe'
: selected
? '#eff6ff'
: '#f9fafb',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.25rem',
cursor: isBusy ? 'default' : isCopyDragging ? 'copy' : 'grab',
opacity: isBusy ? 0.7 : 1,
}}
>
{/* selection checkbox */}
<button
type="button"
onClick={handleCheckboxClick(page.id, visualIndex)}
style={{
position: 'absolute',
top: '4px',
left: '4px',
width: '20px',
height: '20px',
borderRadius: '0.4rem',
border: '1px solid #9ca3af',
background: selected ? '#2563eb' : 'rgba(255,255,255,0.9)',
color: selected ? 'white' : 'transparent',
fontSize: '0.8rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
cursor: 'pointer',
}}
title="Select page"
>
</button>
{/* left drop indicator */}
{showLeftLine(visualIndex) && (
<div
style={{
position: 'absolute',
left: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: dropIndicatorColor,
}}
/>
)}
{/* right drop indicator */}
{showRightLine(visualIndex) && (
<div
style={{
position: 'absolute',
right: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: dropIndicatorColor,
}}
/>
)}
<div
style={{
width: '110px',
height: '90px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{thumb ? (
<img
src={thumb}
alt={`Page ${page.sourcePageIndex + 1}`}
style={{
maxWidth: '100%',
maxHeight: '100%',
width: 'auto',
height: 'auto',
objectFit: 'contain',
borderRadius: '0.25rem',
border: '1px solid #e5e7eb',
background: 'white',
}}
/>
) : (
<div
style={{
width: '60px',
height: '80px',
borderRadius: '0.25rem',
border: '1px dashed #d1d5db',
background: '#f3f4f6',
}}
/>
)}
</div>
<span style={{ fontSize: '0.8rem' }}>Page {page.sourcePageIndex + 1}</span>
<span style={{ fontSize: '0.7rem', color: '#6b7280' }}>
Pos {visualIndex + 1} · Rot {rotation}°
</span>
<div
style={{
display: 'flex',
gap: '0.25rem',
marginTop: '0.25rem',
}}
>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRotateClickClockwise(page.id)();
}}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.4rem',
fontSize: '0.75rem',
background: '#e5e7eb',
cursor: 'pointer',
}}
>
90°
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRotateClickCounterclockwise(page.id)();
}}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.4rem',
fontSize: '0.75rem',
background: '#e5e7eb',
cursor: 'pointer',
}}
>
90°
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(page.id)();
}}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.4rem',
fontSize: '0.75rem',
background: '#fecaca',
color: '#b91c1c',
cursor: 'pointer',
}}
title="Remove this page from the exported PDF"
>
</button>
</div>
</div>
);
})}
{/* end slot for dropping after the last card */}
{pages.length > 0 && (
<div
onDragOver={handleEndSlotDragOver}
onDrop={handleDrop}
style={{
width: '20px',
height: '120px',
position: 'relative',
alignSelf: 'stretch',
}}
>
{showEndLine() && (
<div
style={{
position: 'absolute',
left: '8px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: dropIndicatorColor,
}}
/>
)}
</div>
)}
</div>
onOpenPreview={onOpenPreview}
onToggleSelect={handleCheckboxClick}
onRotateClockwise={onRotateClockwise}
onRotateCounterclockwise={onRotateCounterclockwise}
onDelete={handleDeleteClick}
/>
</div>
{copyDialogOpen && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="copy-pages-dialog-title"
onPointerDown={(e) => {
if (e.target === e.currentTarget) {
handleCopyDialogCancel();
}
}}
style={{
position: 'fixed',
inset: 0,
zIndex: 60,
background: 'rgba(15, 23, 42, 0.55)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
}}
>
<form
onSubmit={handleCopyDialogConfirm}
style={{
width: '100%',
maxWidth: '420px',
background: 'white',
borderRadius: '0.75rem',
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.35)',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '0.75rem',
}}
>
<h2
id="copy-pages-dialog-title"
style={{
margin: 0,
fontSize: '1rem',
}}
>
Copy selected pages
</h2>
<button
type="button"
onClick={handleCopyDialogCancel}
style={{
border: 'none',
borderRadius: '999px',
width: '1.8rem',
height: '1.8rem',
background: '#e5e7eb',
color: '#111827',
cursor: 'pointer',
fontSize: '1.1rem',
lineHeight: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
aria-label="Close copy dialog"
>
×
</button>
</div>
<p
style={{
margin: 0,
fontSize: '0.9rem',
color: '#4b5563',
}}
>
Copy{' '}
<strong>
{selectedPageIds.length === 1
? '1 selected page'
: `${selectedPageIds.length} selected pages`}
</strong>{' '}
to a new position.
</p>
<label
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
fontSize: '0.9rem',
}}
>
Insert before position
<input
type="number"
min={1}
max={pages.length + 1}
value={copyTargetPosition}
autoFocus
onChange={(e) => {
setCopyTargetPosition(e.target.value);
setCopyDialogError(null);
}}
style={{
padding: '0.45rem 0.55rem',
borderRadius: '0.5rem',
border: '1px solid #d1d5db',
fontSize: '0.95rem',
}}
/>
</label>
<div
style={{
fontSize: '0.8rem',
color: '#6b7280',
lineHeight: 1.4,
}}
>
<div>1 = before the first page</div>
<div>{pages.length + 1} = after the last page</div>
</div>
{copyDialogError && (
<div
style={{
borderRadius: '0.5rem',
background: '#fef2f2',
border: '1px solid #fecaca',
color: '#b91c1c',
padding: '0.5rem',
fontSize: '0.85rem',
}}
>
{copyDialogError}
</div>
)}
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '0.5rem',
marginTop: '0.25rem',
}}
>
<button
type="button"
onClick={handleCopyDialogCancel}
style={{
border: 'none',
borderRadius: '0.5rem',
padding: '0.45rem 0.8rem',
background: '#e5e7eb',
color: '#111827',
cursor: 'pointer',
fontSize: '0.9rem',
}}
>
Cancel
</button>
<button
type="submit"
style={{
border: 'none',
borderRadius: '0.5rem',
padding: '0.45rem 0.8rem',
background: '#16a34a',
color: 'white',
cursor: 'pointer',
fontSize: '0.9rem',
}}
>
Copy pages
</button>
</div>
</form>
</div>
<CopyPagesDialog
selectedCount={selectedPageIds.length}
pageCount={pages.length}
targetPosition={copyTargetPosition}
error={copyDialogError}
onTargetPositionChange={handleCopyTargetPositionChange}
onCancel={handleCopyDialogCancel}
onConfirm={handleCopyDialogConfirm}
/>
)}
</>
);