Spaces:
Paused
Paused
| 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> | |
| ); | |
| }; | |