import Cropper from "react-easy-crop"; import { useState, useCallback, useRef } from "react"; export default function ImageUploader({target, id, buttonMsg, handleAvatarChange, imageRef}){ const imageFileRef = useRef<HTMLInputElement>(); const [imageDataUrl, setImageDataUrl] = useState<string>(); const [croppedAreaPixels, setCroppedAreaPixels] = useState(); const [rotation] = useState(1); const [crop, setCrop] = useState({ x: 0, y: 0 }) const [zoom, setZoom] = useState(1) const [imageLoaded, setImageLoaded] = useState(false); const [isImageShown, setIsImageShown] = useState(false); const [shownImage, setShownImage] = useState<string>(); const [imageUploadModalOpen, setImageUploadModalOpen] = useState(false); // TODO // PUSH cropped image to the database in column = target const openUploaderModal = () => { imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false) setImageUploadModalOpen(!imageUploadModalOpen) } const closeImageUploadModal = () => { setImageUploadModalOpen(false); }; async function ImageUploadHandler() { console.log(imageFileRef.current.files[0]); const img = await readFile(imageFileRef.current.files[0]); console.log(img); setImageDataUrl(img); CropHandler(); } const readFile = (file) => { return new Promise((resolve) => { const reader = new FileReader() reader.addEventListener('load', () => resolve(reader.result), false) reader.readAsDataURL(file) }) } const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { setCroppedAreaPixels(croppedAreaPixels) }, []) const CropHandler = () => { setCrop({ x: 0, y: 0 }); setZoom(1); setImageLoaded(true); } const createImage = (url) => new Promise<HTMLImageElement>((resolve, reject) => { const image = new Image() image.addEventListener('load', () => resolve(image)) image.addEventListener('error', error => reject(error)) image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox image.src = url }) function getRadianAngle(degreeValue) { return (degreeValue * Math.PI) / 180 } async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { const image = await createImage(imageSrc) const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') const maxSize = Math.max(image.width, image.height) const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)) // set each dimensions to double largest dimension to allow for a safe area for the // image to rotate in without being clipped by canvas context canvas.width = safeArea canvas.height = safeArea // translate canvas context to a central location on image to allow rotating around the center. ctx.translate(safeArea / 2, safeArea / 2) ctx.rotate(getRadianAngle(rotation)) ctx.translate(-safeArea / 2, -safeArea / 2) // draw rotated image and store data. ctx.drawImage( image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5 ) const data = ctx.getImageData(0, 0, safeArea, safeArea) // set canvas width to final desired crop size - this will clear existing context canvas.width = pixelCrop.width canvas.height = pixelCrop.height // paste generated rotate image with correct offsets for x,y crop values. ctx.putImageData( data, Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) ) // As Base64 string return canvas.toDataURL('image/jpeg'); // As a blob // return new Promise(resolve => { // canvas.toBlob(file => { // resolve(URL.createObjectURL(file)) // }, 'image/jpeg') // }) } const showCroppedImage = useCallback(async () => { try { const croppedImage = await getCroppedImg( imageDataUrl, croppedAreaPixels, rotation ) setIsImageShown(true) setShownImage(croppedImage) setImageLoaded(false) handleAvatarChange(croppedImage) closeImageUploadModal() } catch (e) { console.error(e) } }, [croppedAreaPixels, rotation]) return ( <div className="flex justify-center items-center"> <button type="button" className="ml-4 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;" onClick={openUploaderModal} > {buttonMsg} </button> { imageUploadModalOpen && <div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> ​ </span> <div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-6"> <div className="sm:flex sm:items-start mb-4"> <div className="mt-3 text-center sm:mt-0 sm:text-left"> <h3 className="text-lg leading-6 font-bold text-gray-900" id="modal-title"> Upload an avatar </h3> </div> </div> <div className="mb-4"> <div className="cropper mt-6 flex flex-col justify-center items-center p-8 bg-gray-50"> {!imageLoaded && <div className="flex justify-start items-center bg-gray-500 max-h-20 h-20 w-20 rounded-full"> {!isImageShown && <p className="sm:text-xs text-sm text-white w-full text-center">No {target}</p> } {isImageShown && <img className="h-20 w-20 rounded-full" src={shownImage} alt={target} /> } </div> } {imageLoaded && <div className="crop-container max-h-40 h-40 w-40 rounded-full"> <div className="relative h-40 w-40 rounded-full"> <Cropper image={imageDataUrl} crop={crop} zoom={zoom} aspect={1 / 1} onCropChange={setCrop} onCropComplete={onCropComplete} onZoomChange={setZoom} /> </div> </div> } <label htmlFor={id} className="mt-4 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;">Choose a file...</label> <input onChange={ImageUploadHandler} ref={imageFileRef} type="file" id={id} name={id} placeholder="Upload image" className="mt-4 cursor-pointer opacity-0 absolute" /> </div> </div> <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <button type="button" className="btn btn-primary" onClick={showCroppedImage}> Save </button> <button onClick={closeImageUploadModal} type="button" className="btn btn-white mr-2"> Cancel </button> </div> </div> </div> </div> } </div> ) }