chore: Change crop to remove locked aspect ratio
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m21s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 22s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m21s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 22s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
This commit is contained in:
@@ -25,11 +25,13 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
const imageAreaRef = useRef<HTMLDivElement>(null);
|
||||
const [imageMaxHeight, setImageMaxHeight] = useState(0);
|
||||
|
||||
const { cropArea, isDragging, resetCrop, executeCrop, handleDragStart } =
|
||||
const { cropArea, cropDrawn, isDragging, resetCrop, executeCrop, handleDragStart, handleDrawStart } =
|
||||
useImageCrop({
|
||||
aspectRatio: lockAspectRatio ? aspectRatio : undefined,
|
||||
});
|
||||
|
||||
const showCropArea = cropDrawn || (isDragging && cropArea.width > 1 && cropArea.height > 1);
|
||||
|
||||
// Measure available height for the image so the crop container
|
||||
// matches the rendered image exactly (fixes mobile crop offset)
|
||||
useEffect(() => {
|
||||
@@ -108,7 +110,23 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
{/* Draw surface for free-form rectangle drawing */}
|
||||
{!cropDrawn && (
|
||||
<Box
|
||||
onMouseDown={handleDrawStart}
|
||||
onTouchStart={handleDrawStart}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
cursor: 'crosshair',
|
||||
zIndex: 5,
|
||||
touchAction: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Dark overlay outside crop area */}
|
||||
{showCropArea && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -161,8 +179,10 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Crop area with handles */}
|
||||
{/* Crop area border and handles */}
|
||||
{showCropArea && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -174,6 +194,9 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{/* Handles only appear after drawing is complete */}
|
||||
{cropDrawn && (
|
||||
<>
|
||||
{/* Move handle (center area) */}
|
||||
<CropHandleArea
|
||||
handle="move"
|
||||
@@ -196,6 +219,8 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
<CropHandle handle="s" onDragStart={handleDragStart} position="bottom" />
|
||||
<CropHandle handle="w" onDragStart={handleDragStart} position="left" />
|
||||
<CropHandle handle="e" onDragStart={handleDragStart} position="right" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Grid lines for alignment */}
|
||||
<Box
|
||||
@@ -221,13 +246,14 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Instructions */}
|
||||
<Box sx={{ px: 2, py: 1, textAlign: 'center' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Drag to adjust crop area
|
||||
{cropDrawn ? 'Drag handles to adjust crop area' : 'Tap and drag to select crop area'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -255,7 +281,7 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
onClick={handleReset}
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{ color: 'white' }}
|
||||
disabled={isProcessing}
|
||||
disabled={isProcessing || !cropDrawn}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
@@ -271,7 +297,7 @@ export const CropTool: React.FC<CropToolProps> = ({
|
||||
|
||||
<IconButton
|
||||
onClick={handleConfirm}
|
||||
disabled={isProcessing}
|
||||
disabled={isProcessing || !cropDrawn}
|
||||
aria-label="Confirm crop"
|
||||
sx={{
|
||||
width: 56,
|
||||
|
||||
@@ -18,16 +18,20 @@ interface UseImageCropOptions {
|
||||
interface UseImageCropReturn {
|
||||
/** Current crop area */
|
||||
cropArea: CropArea;
|
||||
/** Whether user has drawn a crop rectangle */
|
||||
cropDrawn: boolean;
|
||||
/** Whether user is actively dragging */
|
||||
isDragging: boolean;
|
||||
/** Set crop area */
|
||||
setCropArea: (area: CropArea) => void;
|
||||
/** Reset crop to initial/default */
|
||||
/** Reset crop to drawing mode */
|
||||
resetCrop: () => void;
|
||||
/** Execute crop and return cropped blob */
|
||||
executeCrop: (imageSrc: string, mimeType?: string) => Promise<Blob>;
|
||||
/** Handle drag start for crop handles */
|
||||
handleDragStart: (handle: CropHandle, event: React.MouseEvent | React.TouchEvent) => void;
|
||||
/** Handle draw start for free-form rectangle drawing */
|
||||
handleDrawStart: (event: React.MouseEvent | React.TouchEvent) => void;
|
||||
/** Handle move during drag */
|
||||
handleMove: (event: MouseEvent | TouchEvent) => void;
|
||||
/** Handle drag end */
|
||||
@@ -78,12 +82,22 @@ export function useImageCrop(options: UseImageCropOptions = {}): UseImageCropRet
|
||||
const [cropArea, setCropAreaState] = useState<CropArea>(
|
||||
getAspectRatioAdjustedCrop(initialCrop)
|
||||
);
|
||||
const [cropDrawn, setCropDrawn] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const activeHandleRef = useRef<CropHandle | null>(null);
|
||||
const startPosRef = useRef({ x: 0, y: 0 });
|
||||
const startCropRef = useRef<CropArea>(cropArea);
|
||||
const containerRef = useRef<{ width: number; height: number }>({ width: 100, height: 100 });
|
||||
const containerRef = useRef<{ width: number; height: number; left: number; top: number }>({
|
||||
width: 100, height: 100, left: 0, top: 0,
|
||||
});
|
||||
const isDrawingRef = useRef(false);
|
||||
const drawOriginRef = useRef({ x: 0, y: 0 });
|
||||
const cropAreaRef = useRef(cropArea);
|
||||
|
||||
useEffect(() => {
|
||||
cropAreaRef.current = cropArea;
|
||||
}, [cropArea]);
|
||||
|
||||
const setCropArea = useCallback(
|
||||
(area: CropArea) => {
|
||||
@@ -94,6 +108,7 @@ export function useImageCrop(options: UseImageCropOptions = {}): UseImageCropRet
|
||||
|
||||
const resetCrop = useCallback(() => {
|
||||
setCropAreaState(getAspectRatioAdjustedCrop(initialCrop));
|
||||
setCropDrawn(false);
|
||||
}, [initialCrop, getAspectRatioAdjustedCrop]);
|
||||
|
||||
const constrainCrop = useCallback(
|
||||
@@ -136,19 +151,75 @@ export function useImageCrop(options: UseImageCropOptions = {}): UseImageCropRet
|
||||
const container = target.closest('[data-crop-container]');
|
||||
if (container) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
containerRef.current = { width: rect.width, height: rect.height };
|
||||
containerRef.current = { width: rect.width, height: rect.height, left: rect.left, top: rect.top };
|
||||
}
|
||||
},
|
||||
[cropArea]
|
||||
);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
if (!activeHandleRef.current || !isDragging) return;
|
||||
const handleDrawStart = useCallback(
|
||||
(event: React.MouseEvent | React.TouchEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const container = target.closest('[data-crop-container]');
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
containerRef.current = { width: rect.width, height: rect.height, left: rect.left, top: rect.top };
|
||||
|
||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
const x = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
|
||||
const y = Math.max(0, Math.min(100, ((clientY - rect.top) / rect.height) * 100));
|
||||
|
||||
startPosRef.current = { x: clientX, y: clientY };
|
||||
drawOriginRef.current = { x, y };
|
||||
|
||||
setCropAreaState({ x, y, width: 0, height: 0 });
|
||||
|
||||
isDrawingRef.current = true;
|
||||
activeHandleRef.current = null;
|
||||
setIsDragging(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
// Free-form drawing mode: compute rectangle from origin to current pointer
|
||||
if (isDrawingRef.current) {
|
||||
const currentX = Math.max(0, Math.min(100,
|
||||
((clientX - containerRef.current.left) / containerRef.current.width) * 100));
|
||||
const currentY = Math.max(0, Math.min(100,
|
||||
((clientY - containerRef.current.top) / containerRef.current.height) * 100));
|
||||
|
||||
const originX = drawOriginRef.current.x;
|
||||
const originY = drawOriginRef.current.y;
|
||||
|
||||
let newCrop: CropArea = {
|
||||
x: Math.min(originX, currentX),
|
||||
y: Math.min(originY, currentY),
|
||||
width: Math.abs(currentX - originX),
|
||||
height: Math.abs(currentY - originY),
|
||||
};
|
||||
|
||||
if (aspectRatio) {
|
||||
newCrop.height = newCrop.width / aspectRatio;
|
||||
}
|
||||
|
||||
setCropAreaState(newCrop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeHandleRef.current) return;
|
||||
|
||||
// Calculate delta as percentage of container
|
||||
const deltaX = ((clientX - startPosRef.current.x) / containerRef.current.width) * 100;
|
||||
const deltaY = ((clientY - startPosRef.current.y) / containerRef.current.height) * 100;
|
||||
@@ -234,13 +305,20 @@ export function useImageCrop(options: UseImageCropOptions = {}): UseImageCropRet
|
||||
|
||||
setCropAreaState(constrainCrop(newCrop));
|
||||
},
|
||||
[isDragging, constrainCrop]
|
||||
[isDragging, constrainCrop, aspectRatio]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
if (isDrawingRef.current) {
|
||||
isDrawingRef.current = false;
|
||||
const area = cropAreaRef.current;
|
||||
if (area.width >= minSize && area.height >= minSize) {
|
||||
setCropDrawn(true);
|
||||
}
|
||||
}
|
||||
activeHandleRef.current = null;
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
}, [minSize]);
|
||||
|
||||
// Add global event listeners for drag
|
||||
useEffect(() => {
|
||||
@@ -320,11 +398,13 @@ export function useImageCrop(options: UseImageCropOptions = {}): UseImageCropRet
|
||||
|
||||
return {
|
||||
cropArea,
|
||||
cropDrawn,
|
||||
isDragging,
|
||||
setCropArea,
|
||||
resetCrop,
|
||||
executeCrop,
|
||||
handleDragStart,
|
||||
handleDrawStart,
|
||||
handleMove,
|
||||
handleDragEnd,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user