Layzur_Pro / src /components /ImageCropper.jsx
Shinhati2023's picture
Upload ImageCropper.jsx
9bb286f verified
Raw
History Blame Contribute Delete
4.03 kB
import React, { useState, useCallback } from 'react';
import Cropper, { Area, Point } from 'react-easy-crop';
import { Check, X, ZoomIn, ZoomOut } from 'lucide-react';
import { motion } from 'motion/react';
interface ImageCropperProps {
image;
aspect;
onCropComplete(croppedImage) => void;
onCancel() => void;
circularCrop?;
}
export const ImageCropper.FC = ({
image,
aspect,
onCropComplete,
onCancel,
circularCrop = false
}) => {
const [crop, setCrop] = useState({ x});
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
const onCropChange = (crop) => {
setCrop(crop);
};
const onZoomChange = (zoom) => {
setZoom(zoom);
};
const onCropCompleteInternal = useCallback((_croppedArea) => {
setCroppedAreaPixels(croppedAreaPixels);
}, []);
const createImage = (url)=>
new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener('load', () => resolve(image));
image.addEventListener('error', (error) => reject(error));
image.setAttribute('crossOrigin', 'anonymous');
image.src = url;
});
const getCroppedImg = async (
imageSrc)=> {
const image = await createImage(imageSrc);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('No 2d context');
}
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height
);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('Canvas is empty'));
return;
}
resolve(blob);
}, 'image/jpeg');
});
};
const handleDone = async () => {
if (croppedAreaPixels) {
try {
const croppedImage = await getCroppedImg(image, croppedAreaPixels);
onCropComplete(croppedImage);
} catch (e) {
console.error(e);
}
}
};
return (
<motion.div
initial={{ opacity}}
animate={{ opacity}}
exit={{ opacity}}
className="fixed inset-0 z-[100] bg-black flex flex-col"
>
<div className="relative flex-1 bg-neutral-900">
<Cropper
image={image}
crop={crop}
zoom={zoom}
aspect={aspect}
cropShape={circularCrop ? 'round' 'rect'}
showGrid={false}
onCropChange={onCropChange}
onCropComplete={onCropCompleteInternal}
onZoomChange={onZoomChange}
/>
</div>
<div className="glass-thick p-6 space-y-6">
<div className="flex items-center gap-4">
<ZoomOut size={20} className="text-white/40" />
<input
type="range"
value={zoom}
min={1}
max={3}
step={0.1}
aria-labelledby="Zoom"
onChange={(e) => setZoom(Number(e.target.value))}
className="flex-1 h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-orange-500"
/>
<ZoomIn size={20} className="text-white/40" />
</div>
<div className="flex items-center justify-between gap-4">
<button
onClick={onCancel}
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl bg-white/5 hover-white/10 transition-colors font-bold"
>
<X size={20} />
Cancel
</button>
<button
onClick={handleDone}
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl bg-orange-500 shadow-lg shadow-orange-500/20 active-95 transition-transform font-bold"
>
<Check size={20} />
Apply
</button>
</div>
</div>
</motion.div>
);
};