marco.pms.web/src/components/Expenses/PreviewDocument.jsx

170 lines
4.5 KiB
JavaScript

import { useState, useRef, useEffect } from "react";
const PreviewDocument = ({ files = [] }) => {
const images = Array.isArray(files) ? files : [files];
const [index, setIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0);
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const startPos = useRef({ x: 0, y: 0 });
const MIN_ZOOM = 0.4;
const MAX_ZOOM = 3;
const currentImage = images[index];
// Reset on image change
useEffect(() => {
setRotation(0);
setScale(1);
setPosition({ x: 0, y: 0 });
setLoading(true);
}, [index]);
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, MAX_ZOOM));
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, MIN_ZOOM));
const resetAll = () => {
setRotation(0);
setScale(1);
setPosition({ x: 0, y: 0 });
};
const nextImage = () => {
if (index < images.length - 1) setIndex((i) => i + 1);
};
const prevImage = () => {
if (index > 0) setIndex((i) => i - 1);
};
const handleWheel = (e) => {
e.preventDefault();
if (e.ctrlKey) {
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setScale((prev) => {
let next = prev + delta;
if (next < MIN_ZOOM) next = MIN_ZOOM;
if (next > MAX_ZOOM) next = MAX_ZOOM;
return next;
});
} else {
if (e.deltaY > 0) nextImage();
else prevImage();
}
};
const handleMouseDown = (e) => {
setDragging(true);
startPos.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
};
const handleMouseMove = (e) => {
if (!dragging) return;
setPosition({
x: e.clientX - startPos.current.x,
y: e.clientY - startPos.current.y,
});
};
const handleMouseUp = () => setDragging(false);
const handleDoubleClick = () => resetAll();
return (
<>
{/* Top Controls */}
<div className="d-flex justify-content-start align-items-center mb-2">
{/* Left */}
<div className="d-flex gap-3">
<i
className="bx bx-rotate-right cursor-pointer fs-4"
onClick={() => setRotation((prev) => prev + 90)}
title="Rotate"
/>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
onClick={zoomIn}
title="Zoom In"
/>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
onClick={zoomOut}
title="Zoom Out"
/>
<i
className="bx bx-reset cursor-pointer fs-4"
onClick={resetAll}
title="Reset"
/>
</div>
</div>
<div
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onDoubleClick={handleDoubleClick}
className="position-relative d-flex justify-content-center align-items-center bg-light-secondary overflow-hidden"
style={{
minHeight: "70vh",
userSelect: "none",
borderRadius: "10px",
}}
>
{loading && <div className="text-secondary">Loading...</div>}
<img
src={currentImage?.preSignedUrl}
alt="Preview"
draggable="false"
style={{
maxHeight: "60vh",
display: loading ? "none" : "block",
transform: `
translate(${position.x}px, ${position.y}px)
scale(${scale})
rotate(${rotation}deg)
`,
transition: dragging ? "none" : "transform 0.2s ease",
cursor: dragging ? "grabbing" : "grab",
}}
onLoad={() => setLoading(false)}
/>
</div>
<div className="d-flex justify-content-between">
<div className="text-center text-muted mt-2 small">
Scroll = change image | Double click = reset
</div>
<div className="d-flex align-items-center gap-2">
<i
className="bx bx-chevron-left cursor-pointer fs-4"
onClick={prevImage}
/>
<span>
{index + 1} / {images.length}
</span>
<i
className="bx bx-chevron-right cursor-pointer fs-4"
onClick={nextImage}
/>
</div>
</div>
</>
);
};
export default PreviewDocument;