import React, { useState, useRef, useCallback } from "react"; import { X, Check, Move } from "lucide-react"; import { Button } from "../ui/button"; interface ImageEditorProps { isOpen: boolean; onClose: () => void; imageUrl: string; onSave: (editedImageBlob: Blob) => void; } interface CropArea { x: number; y: number; width: number; height: number; } export function ImageEditor({ isOpen, onClose, imageUrl, onSave, }: ImageEditorProps) { const [cropArea, setCropArea] = useState({ x: 50, y: 50, width: 150, height: 150, }); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [resizeDirection, setResizeDirection] = useState< "se" | "sw" | "ne" | "nw" | null >(null); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const canvasRef = useRef(null); const containerRef = useRef(null); // Helper function to get coordinates from mouse or touch event const getEventCoordinates = (e: MouseEvent | TouchEvent) => { if ("touches" in e && e.touches.length > 0) { return { x: e.touches[0].clientX, y: e.touches[0].clientY }; } return { x: (e as MouseEvent).clientX, y: (e as MouseEvent).clientY }; }; const handlePointerDown = ( e: React.MouseEvent | React.TouchEvent, action: "drag" | "resize", direction?: "se" | "sw" | "ne" | "nw" ) => { e.preventDefault(); if (!containerRef.current) return; const coords = getEventCoordinates( e.nativeEvent as MouseEvent | TouchEvent ); const rect = containerRef.current.getBoundingClientRect(); const relativeX = coords.x - rect.left; const relativeY = coords.y - rect.top; if (action === "drag") { setIsDragging(true); setDragStart({ x: relativeX - cropArea.x, y: relativeY - cropArea.y, }); } else { setIsResizing(true); setResizeDirection(direction || "se"); setDragStart({ x: relativeX, y: relativeY, }); } }; const handlePointerMove = useCallback( (e: MouseEvent | TouchEvent) => { if (!containerRef.current) return; // Prevent default behavior for touch events to avoid scrolling if ("touches" in e) { e.preventDefault(); } const coords = getEventCoordinates(e); const rect = containerRef.current.getBoundingClientRect(); // Convert global coordinates to container-relative coordinates const relativeX = coords.x - rect.left; const relativeY = coords.y - rect.top; if (isDragging) { const newX = relativeX - dragStart.x; const newY = relativeY - dragStart.y; setCropArea((prev) => ({ ...prev, x: Math.max(0, Math.min(rect.width - prev.width, newX)), y: Math.max(0, Math.min(rect.height - prev.height, newY)), })); } else if (isResizing && resizeDirection) { const deltaX = relativeX - dragStart.x; const deltaY = relativeY - dragStart.y; setCropArea((prev) => { let newX = prev.x; let newY = prev.y; let newWidth = prev.width; let newHeight = prev.height; switch (resizeDirection) { case "se": // Southeast (bottom-right) newWidth = Math.max( 30, Math.min(rect.width - prev.x, prev.width + deltaX) ); newHeight = Math.max( 30, Math.min(rect.height - prev.y, prev.height + deltaY) ); break; case "sw": // Southwest (bottom-left) newWidth = Math.max(30, prev.width - deltaX); newHeight = Math.max( 30, Math.min(rect.height - prev.y, prev.height + deltaY) ); newX = Math.max( 0, Math.min(prev.x + prev.width - newWidth, prev.x + deltaX) ); break; case "ne": // Northeast (top-right) newWidth = Math.max( 30, Math.min(rect.width - prev.x, prev.width + deltaX) ); newHeight = Math.max(30, prev.height - deltaY); newY = Math.max( 0, Math.min(prev.y + prev.height - newHeight, prev.y + deltaY) ); break; case "nw": // Northwest (top-left) newWidth = Math.max(30, prev.width - deltaX); newHeight = Math.max(30, prev.height - deltaY); newX = Math.max( 0, Math.min(prev.x + prev.width - newWidth, prev.x + deltaX) ); newY = Math.max( 0, Math.min(prev.y + prev.height - newHeight, prev.y + deltaY) ); break; } return { x: newX, y: newY, width: newWidth, height: newHeight, }; }); // Update drag start for continuous resizing - using relative coordinates setDragStart({ x: relativeX, y: relativeY }); } }, [isDragging, isResizing, resizeDirection, dragStart] ); const handlePointerUp = useCallback(() => { setIsDragging(false); setIsResizing(false); setResizeDirection(null); }, []); // Add event listeners for both mouse and touch events React.useEffect(() => { if (isDragging || isResizing) { // Mouse events document.addEventListener("mousemove", handlePointerMove); document.addEventListener("mouseup", handlePointerUp); // Touch events with passive: false to allow preventDefault document.addEventListener("touchmove", handlePointerMove, { passive: false, }); document.addEventListener("touchend", handlePointerUp); document.addEventListener("touchcancel", handlePointerUp); return () => { document.removeEventListener("mousemove", handlePointerMove); document.removeEventListener("mouseup", handlePointerUp); document.removeEventListener("touchmove", handlePointerMove); document.removeEventListener("touchend", handlePointerUp); document.removeEventListener("touchcancel", handlePointerUp); }; } }, [isDragging, isResizing, handlePointerMove, handlePointerUp]); // Initialize crop area to center when component opens React.useEffect(() => { if (isOpen && containerRef.current && imageUrl) { const img = new Image(); img.crossOrigin = "anonymous"; // Add this to handle CORS for external images img.onload = () => { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; // Calculate actual image display dimensions with object-contain const containerAspect = rect.width / rect.height; const imageAspect = img.width / img.height; let displayWidth, displayHeight, offsetX, offsetY; if (imageAspect > containerAspect) { // Image is wider - fit to container width displayWidth = rect.width; displayHeight = rect.width / imageAspect; offsetX = 0; offsetY = (rect.height - displayHeight) / 2; } else { // Image is taller - fit to container height displayWidth = rect.height * imageAspect; displayHeight = rect.height; offsetX = (rect.width - displayWidth) / 2; offsetY = 0; } // Set initial crop area to center of actual image display area const initialSize = Math.min(displayWidth, displayHeight) * 0.6; setCropArea({ x: offsetX + (displayWidth - initialSize) / 2, y: offsetY + (displayHeight - initialSize) / 2, width: initialSize, height: initialSize, }); }; img.onerror = () => { console.error("Failed to load image for initial display"); // Set a default crop area if image fails to load setCropArea({ x: 50, y: 50, width: 150, height: 150, }); }; img.src = imageUrl; } }, [isOpen, imageUrl]); const applyCropAndRotation = useCallback(() => { if (!canvasRef.current || !containerRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; const img = new Image(); img.crossOrigin = "anonymous"; // Add this to handle CORS for external images img.onload = () => { // Get container dimensions const containerRect = containerRef.current?.getBoundingClientRect(); if (!containerRect) return; // Calculate actual image display dimensions with object-contain const containerAspect = containerRect.width / containerRect.height; const imageAspect = img.width / img.height; let displayWidth, displayHeight, offsetX, offsetY; if (imageAspect > containerAspect) { // Image is wider - fit to container width displayWidth = containerRect.width; displayHeight = containerRect.width / imageAspect; offsetX = 0; offsetY = (containerRect.height - displayHeight) / 2; } else { // Image is taller - fit to container height displayWidth = containerRect.height * imageAspect; displayHeight = containerRect.height; offsetX = (containerRect.width - displayWidth) / 2; offsetY = 0; } // Calculate scale factors based on actual displayed image size const scaleX = img.width / displayWidth; const scaleY = img.height / displayHeight; // Adjust crop coordinates to account for image offset const adjustedCropX = (cropArea.x - offsetX) * scaleX; const adjustedCropY = (cropArea.y - offsetY) * scaleY; const adjustedCropWidth = cropArea.width * scaleX; const adjustedCropHeight = cropArea.height * scaleY; // Crop rectangle expressed in the SOURCE image's native pixels // (adjusted* already scales the on-screen crop back up to the original). const srcX = Math.max(0, adjustedCropX); const srcY = Math.max(0, adjustedCropY); const srcWidth = Math.min(adjustedCropWidth, img.width - srcX); const srcHeight = Math.min(adjustedCropHeight, img.height - srcY); // Size the canvas to the full-resolution crop region, NOT the editor's // on-screen crop size. Using the display size here was downscaling phone // photos to a few hundred pixels, which is what made uploads blurry. // The backend still caps the final image to 1600px. canvas.width = Math.max(1, Math.round(srcWidth)); canvas.height = Math.max(1, Math.round(srcHeight)); // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the cropped portion at native resolution ctx.drawImage( img, srcX, srcY, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height ); // Convert to blob and save canvas.toBlob( (blob) => { if (blob) { onSave(blob); } onClose(); }, "image/jpeg", 0.92 ); }; img.onerror = () => { console.error("Failed to load image for cropping"); // Still close the editor on error onClose(); }; img.src = imageUrl; }, [cropArea, imageUrl, onSave, onClose]); if (!isOpen) return null; return (
{/* Header */}

ویرایش تصویر

{/* Image Editor Area */}
{/* Background Image */} Edit {/* Crop Overlay */}
handlePointerDown(e, "drag")} onTouchStart={(e) => handlePointerDown(e, "drag")} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); // Handle keyboard interaction if needed } }} > {/* Crop area background */}
{/* Resize handles - all four corners */} {/* Top-left */}
{ e.stopPropagation(); handlePointerDown(e, "resize", "nw"); }} onTouchStart={(e) => { e.stopPropagation(); handlePointerDown(e, "resize", "nw"); }} /> {/* Top-right */}
{ e.stopPropagation(); handlePointerDown(e, "resize", "ne"); }} onTouchStart={(e) => { e.stopPropagation(); handlePointerDown(e, "resize", "ne"); }} /> {/* Bottom-left */}
{ e.stopPropagation(); handlePointerDown(e, "resize", "sw"); }} onTouchStart={(e) => { e.stopPropagation(); handlePointerDown(e, "resize", "sw"); }} /> {/* Bottom-right */}
{ e.stopPropagation(); handlePointerDown(e, "resize", "se"); }} onTouchStart={(e) => { e.stopPropagation(); handlePointerDown(e, "resize", "se"); }} /> {/* Move icon */}
{/* Dark overlay outside crop area */}
{/* Footer */}
{/* Hidden canvas for processing */}
); }