refactoring, linting, formatting
This commit is contained in:
224
src/components/PageWorkspace/CopyPagesDialog.tsx
Normal file
224
src/components/PageWorkspace/CopyPagesDialog.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
interface CopyPagesDialogProps {
|
||||
selectedCount: number;
|
||||
pageCount: number;
|
||||
targetPosition: string;
|
||||
error: string | null;
|
||||
onTargetPositionChange: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (e?: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
|
||||
selectedCount,
|
||||
pageCount,
|
||||
targetPosition,
|
||||
error,
|
||||
onTargetPositionChange,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onCancel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="copy-pages-dialog-title"
|
||||
onPointerDown={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 60,
|
||||
background: "rgba(15, 23, 42, 0.55)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "1rem",
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={onConfirm}
|
||||
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={onCancel}
|
||||
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>
|
||||
{selectedCount === 1
|
||||
? "1 selected page"
|
||||
: `${selectedCount} 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={pageCount + 1}
|
||||
value={targetPosition}
|
||||
autoFocus
|
||||
onChange={(e) => onTargetPositionChange(e.target.value)}
|
||||
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>{pageCount + 1} = after the last page</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "0.5rem",
|
||||
background: "#fef2f2",
|
||||
border: "1px solid #fecaca",
|
||||
color: "#b91c1c",
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
gap: "0.5rem",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyPagesDialog;
|
||||
27
src/components/PageWorkspace/DropIndicator.tsx
Normal file
27
src/components/PageWorkspace/DropIndicator.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
|
||||
interface DropIndicatorProps {
|
||||
side: "left" | "right" | "end";
|
||||
color: string;
|
||||
}
|
||||
|
||||
const DropIndicator: React.FC<DropIndicatorProps> = ({ side, color }) => {
|
||||
const isEnd = side === "end";
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: side === "left" ? "-4px" : isEnd ? "8px" : undefined,
|
||||
right: side === "right" ? "-4px" : undefined,
|
||||
top: "4px",
|
||||
bottom: "4px",
|
||||
width: "3px",
|
||||
borderRadius: "999px",
|
||||
background: color,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropIndicator;
|
||||
213
src/components/PageWorkspace/PageCard.tsx
Normal file
213
src/components/PageWorkspace/PageCard.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import React from "react";
|
||||
import type { PageRef } from "../../pdf/pdfTypes";
|
||||
import DropIndicator from "./DropIndicator";
|
||||
|
||||
interface PageCardProps {
|
||||
page: PageRef;
|
||||
visualIndex: number;
|
||||
thumbnail?: string;
|
||||
selected: boolean;
|
||||
isDraggingCard: boolean;
|
||||
isBusy: boolean;
|
||||
isCopyDragging: boolean;
|
||||
showLeftLine: boolean;
|
||||
showRightLine: boolean;
|
||||
dropIndicatorColor: string;
|
||||
onDragStart: React.DragEventHandler<HTMLDivElement>;
|
||||
onDragEnd: React.DragEventHandler<HTMLDivElement>;
|
||||
onDragOver: React.DragEventHandler<HTMLDivElement>;
|
||||
onOpenPreview: () => void;
|
||||
onToggleSelect: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onRotateClockwise: () => void;
|
||||
onRotateCounterclockwise: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const pageActionButtonStyle: React.CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "999px",
|
||||
padding: "0.15rem 0.4rem",
|
||||
fontSize: "0.75rem",
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const PageCard: React.FC<PageCardProps> = ({
|
||||
page,
|
||||
visualIndex,
|
||||
thumbnail,
|
||||
selected,
|
||||
isDraggingCard,
|
||||
isBusy,
|
||||
isCopyDragging,
|
||||
showLeftLine,
|
||||
showRightLine,
|
||||
dropIndicatorColor,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
onOpenPreview,
|
||||
onToggleSelect,
|
||||
onRotateClockwise,
|
||||
onRotateCounterclockwise,
|
||||
onDelete,
|
||||
}) => {
|
||||
const background = isDraggingCard
|
||||
? isCopyDragging
|
||||
? "#dcfce7"
|
||||
: "#dbeafe"
|
||||
: selected
|
||||
? "#eff6ff"
|
||||
: "#f9fafb";
|
||||
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={onDragOver}
|
||||
onClick={onOpenPreview}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "162px",
|
||||
padding: "0.4rem",
|
||||
borderRadius: "0.5rem",
|
||||
border: "1px solid #e5e7eb",
|
||||
background,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
cursor: isBusy ? "default" : isCopyDragging ? "copy" : "grab",
|
||||
opacity: isBusy ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSelect}
|
||||
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>
|
||||
|
||||
{showLeftLine && <DropIndicator side="left" color={dropIndicatorColor} />}
|
||||
{showRightLine && (
|
||||
<DropIndicator side="right" color={dropIndicatorColor} />
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "110px",
|
||||
height: "90px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
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 {page.rotation}°
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.25rem",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRotateClockwise();
|
||||
}}
|
||||
style={{
|
||||
...pageActionButtonStyle,
|
||||
background: "#e5e7eb",
|
||||
}}
|
||||
>
|
||||
↻ 90°
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRotateCounterclockwise();
|
||||
}}
|
||||
style={{
|
||||
...pageActionButtonStyle,
|
||||
background: "#e5e7eb",
|
||||
}}
|
||||
>
|
||||
↺ 90°
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
style={{
|
||||
...pageActionButtonStyle,
|
||||
background: "#fecaca",
|
||||
color: "#b91c1c",
|
||||
}}
|
||||
title="Remove this page from the exported PDF"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageCard;
|
||||
130
src/components/PageWorkspace/PageGrid.tsx
Normal file
130
src/components/PageWorkspace/PageGrid.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
import type { PageRef } from "../../pdf/pdfTypes";
|
||||
import DropIndicator from "./DropIndicator";
|
||||
import PageCard from "./PageCard";
|
||||
|
||||
interface PageGridProps {
|
||||
pages: PageRef[];
|
||||
thumbnails: Record<string, string>;
|
||||
selectedPageIds: string[];
|
||||
isBusy: boolean;
|
||||
draggingIndex: number | null;
|
||||
dropIndex: number | null;
|
||||
draggingSelectionActive: boolean;
|
||||
isCopyDragging: boolean;
|
||||
dropIndicatorColor: string;
|
||||
onDragStart: (visualIndex: number) => React.DragEventHandler<HTMLDivElement>;
|
||||
onDragEnd: React.DragEventHandler<HTMLDivElement>;
|
||||
onCardDragOver: (
|
||||
visualIndex: number,
|
||||
) => React.DragEventHandler<HTMLDivElement>;
|
||||
onEndSlotDragOver: React.DragEventHandler<HTMLDivElement>;
|
||||
onDrop: React.DragEventHandler<HTMLDivElement>;
|
||||
onOpenPreview: (pageId: string) => void;
|
||||
onToggleSelect: (
|
||||
pageId: string,
|
||||
visualIndex: number,
|
||||
) => React.MouseEventHandler<HTMLButtonElement>;
|
||||
onRotateClockwise: (pageId: string) => void;
|
||||
onRotateCounterclockwise: (pageId: string) => void;
|
||||
onDelete: (pageId: string) => void;
|
||||
}
|
||||
|
||||
const PageGrid: React.FC<PageGridProps> = ({
|
||||
pages,
|
||||
thumbnails,
|
||||
selectedPageIds,
|
||||
isBusy,
|
||||
draggingIndex,
|
||||
dropIndex,
|
||||
draggingSelectionActive,
|
||||
isCopyDragging,
|
||||
dropIndicatorColor,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onCardDragOver,
|
||||
onEndSlotDragOver,
|
||||
onDrop,
|
||||
onOpenPreview,
|
||||
onToggleSelect,
|
||||
onRotateClockwise,
|
||||
onRotateCounterclockwise,
|
||||
onDelete,
|
||||
}) => {
|
||||
const isSelected = (pageId: string) => selectedPageIds.includes(pageId);
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.5rem",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{pages.map((page, visualIndex) => {
|
||||
const selected = isSelected(page.id);
|
||||
const isDraggingCard =
|
||||
draggingIndex != null &&
|
||||
((draggingSelectionActive && selected) ||
|
||||
(!draggingSelectionActive && visualIndex === draggingIndex));
|
||||
|
||||
return (
|
||||
<PageCard
|
||||
key={page.id}
|
||||
page={page}
|
||||
visualIndex={visualIndex}
|
||||
thumbnail={thumbnails[page.id]}
|
||||
selected={selected}
|
||||
isDraggingCard={isDraggingCard}
|
||||
isBusy={isBusy}
|
||||
isCopyDragging={isCopyDragging}
|
||||
showLeftLine={showLeftLine(visualIndex)}
|
||||
showRightLine={showRightLine(visualIndex)}
|
||||
dropIndicatorColor={dropIndicatorColor}
|
||||
onDragStart={onDragStart(visualIndex)}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={onCardDragOver(visualIndex)}
|
||||
onOpenPreview={() => onOpenPreview(page.id)}
|
||||
onToggleSelect={onToggleSelect(page.id, visualIndex)}
|
||||
onRotateClockwise={() => onRotateClockwise(page.id)}
|
||||
onRotateCounterclockwise={() => onRotateCounterclockwise(page.id)}
|
||||
onDelete={() => onDelete(page.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{pages.length > 0 && (
|
||||
<div
|
||||
onDragOver={onEndSlotDragOver}
|
||||
onDrop={onDrop}
|
||||
style={{
|
||||
width: "20px",
|
||||
height: "120px",
|
||||
position: "relative",
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
>
|
||||
{showEndLine() && (
|
||||
<DropIndicator side="end" color={dropIndicatorColor} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageGrid;
|
||||
112
src/components/PageWorkspace/PageSelectionToolbar.tsx
Normal file
112
src/components/PageWorkspace/PageSelectionToolbar.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React from "react";
|
||||
|
||||
interface PageSelectionToolbarProps {
|
||||
selectedCount: number;
|
||||
onCopySelected: () => void;
|
||||
onDeleteSelected: () => void;
|
||||
onSelectAll: () => void;
|
||||
onClearSelection: () => void;
|
||||
}
|
||||
|
||||
const pillButtonStyle: React.CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "999px",
|
||||
padding: "0.15rem 0.6rem",
|
||||
fontSize: "0.8rem",
|
||||
};
|
||||
|
||||
const PageSelectionToolbar: React.FC<PageSelectionToolbarProps> = ({
|
||||
selectedCount,
|
||||
onCopySelected,
|
||||
onDeleteSelected,
|
||||
onSelectAll,
|
||||
onClearSelection,
|
||||
}) => {
|
||||
const hasSelection = selectedCount > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
Selected: <strong>{selectedCount}</strong>
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.4rem",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{hasSelection && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopySelected}
|
||||
disabled={!hasSelection}
|
||||
style={{
|
||||
...pillButtonStyle,
|
||||
background: "#dcfce7",
|
||||
color: "#166534",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
title="Copy selected pages to another position"
|
||||
>
|
||||
Copy selected
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasSelection && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDeleteSelected}
|
||||
style={{
|
||||
...pillButtonStyle,
|
||||
background: "#fee2e2",
|
||||
color: "#b91c1c",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Delete selected
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelectAll}
|
||||
style={{
|
||||
...pillButtonStyle,
|
||||
background: "#8dcd8d",
|
||||
color: "#111827",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Select all
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearSelection}
|
||||
disabled={!hasSelection}
|
||||
style={{
|
||||
...pillButtonStyle,
|
||||
background: "#e5e7eb",
|
||||
color: hasSelection ? "#111827" : "#6b7280",
|
||||
cursor: hasSelection ? "pointer" : "default",
|
||||
}}
|
||||
>
|
||||
Clear selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSelectionToolbar;
|
||||
Reference in New Issue
Block a user