DEV Community

Mizan-Rifat
Mizan-Rifat

Posted on

Creating an Image Upload Modal with Crop and Rotate Functionality in React

Introduction:

Welcome to this tutorial on creating an image modal with crop and rotate functionality in React! In this blog post, we'll explore how to leverage the power of react-easy-crop library to build an interactive image modal in react similar to those found in popular platforms like GitHub or Facebook.

Image crop modal

here is live demo link
In the live demo, you can experience the full functionality of the image modal with crop and rotate features.

The focus of this tutorial is to develop a solution that minimizes the need for excessive prop passing between components. To achieve this, we'll harness the capabilities of React's context API. By creating a context provider and a custom hook, we can seamlessly share data and functions across our components without the hassle of passing props down the component tree.

So, let's dive in and learn how to create an image modal that allows users to crop and rotate their uploaded images.

Setting Up the Project:

To get started with our image modal implementation, i'll assume you already have a React project set up. For UI i’m using Tailwind CSS. But you can use any UI library as your wish.
For the image cropping and rotating functionality, we'll be utilizing the react-easy-crop library. This library provides a simple and intuitive way to crop and interact with images and videos within a React component.
We will also use the heroicons and classnames libraries in our tutorial. To install all the libraries and their dependencies, open your terminal and navigate to your project's directory. Run the following command:

npm install react-easy-crop classnames @heroicons/react
#or
yarn add react-easy-crop classnames @heroicons/react
Enter fullscreen mode Exit fullscreen mode

Now that we have react-easy-crop and other libraries are installed, let's move on to creating the necessary components and implementing the image modal functionality.

Creating base components:

In this tutorial, we'll create two essential base components Button and Modal which will be needed in our image modal with crop and rotate functionality. Let's take a closer look at each of them:

// src/components/base/Button.jsx

import classNames from 'classnames';

const Button = ({ variant, className, children, ...rest }) => {
  return (
    <button
      type="button"
      className={classNames(className, 'hover:shadow-inner px-4 py-2 text-sm rounded-3xl', {
        'bg-blue-500 text-white hover:bg-blue-700 hover:text-white': variant === 'primary',
        'bg-red-500 text-white hover:bg-red-700 hover:text-white': variant === 'secondary',
        'bg-white text-gray-900 hover:bg-white hover:text-blue-500': variant === 'light'
      })}
      {...rest}
    >
      {children}
    </button>
  );
};

export default Button;

Enter fullscreen mode Exit fullscreen mode

The Button component is a reusable and versatile element that allows us to create different styles of buttons with ease.

//src/components/base/Modal.jsx

import classNames from 'classnames';

const Modal = ({ open, children }) => {
  return (
    <div
      className={classNames('fixed z-10 overflow-y-auto top-0 w-full left-0', {
        hidden: !open
      })}
      id="modal"
    >
      <div className="flex items-center justify-center min-height-100vh pt-4 px-4 pb-20 text-center sm:block sm:p-0">
        <div className="fixed inset-0 transition-opacity">
          <div className="absolute inset-0 bg-gray-900 opacity-75"></div>
          <span className="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
          <div
            className="inline-block align-center bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full"
            role="dialog"
            aria-modal="true"
            aria-labelledby="modal-headline"
          >
            <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">{children}</div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Modal;

Enter fullscreen mode Exit fullscreen mode

The Modal component plays a crucial role in our image modal implementation. It handles the display of content in a modal overlay, providing a clean and focused interface for editing images.

Creating the image crop context:

One of the essential aspects of building our image modal with crop and rotate functionality is to manage the state effectively. To achieve this, we'll create a React context that will provide the necessary data and functions to components that require access to this state.

//src/providers/ImageCropProvider.jsx

/* eslint-disable react-refresh/only-export-components */
import { createContext, useCallback, useContext, useState } from 'react';
import getCroppedImg from '../helpers/cropImage';

export const ImageCropContext = createContext({});

const defaultImage = null;
const defaultCrop = { x: 0, y: 0 };
const defaultRotation = 0;
const defaultZoom = 1;
const defaultCroppedAreaPixels = null;

const ImageCropProvider = ({
  children,
  max_zoom = 3,
  min_zoom = 1,
  zoom_step = 0.1,
  max_rotation = 360,
  min_rotation = 0,
  rotation_step = 5
}) => {
  const [image, setImage] = useState(defaultImage);
  const [crop, setCrop] = useState(defaultCrop);
  const [rotation, setRotation] = useState(defaultRotation);
  const [zoom, setZoom] = useState(defaultZoom);
  const [croppedAreaPixels, setCroppedAreaPixels] = useState(defaultCroppedAreaPixels);

  const onCropComplete = useCallback((_croppedArea, croppedAreaPixels) => {
    setCroppedAreaPixels(croppedAreaPixels);
  }, []);

  const handleZoomIn = () => {
    if (zoom < max_zoom) {
      setZoom(zoom + zoom_step * 2);
    }
  };

  const handleZoomOut = () => {
    if (zoom > min_zoom) {
      setZoom(zoom - zoom_step * 2);
    }
  };

  const handleRotateCw = () => {
    setRotation(rotation + rotation_step);
  };

  const handleRotateAntiCw = () => {
    setRotation(rotation - rotation_step);
  };

  const getProcessedImage = async () => {
    if (image && croppedAreaPixels) {
      const croppedImage = await getCroppedImg(image, croppedAreaPixels, rotation);
      const imageFile = new File([croppedImage.file], `img-${Date.now()}.png`, {
        type: 'image/png'
      });
      return imageFile;
    }
  };

  const resetStates = () => {
    setImage(defaultImage);
    setCrop(defaultCrop);
    setRotation(defaultRotation);
    setZoom(defaultZoom);
    setCroppedAreaPixels(defaultCroppedAreaPixels);
  };

  return (
    <ImageCropContext.Provider
      value={{
        image,
        setImage,
        zoom,
        setZoom,
        rotation,
        setRotation,
        crop,
        setCrop,
        croppedAreaPixels,
        setCroppedAreaPixels,
        onCropComplete,
        getProcessedImage,
        handleZoomIn,
        handleZoomOut,
        handleRotateAntiCw,
        handleRotateCw,
        max_zoom,
        min_zoom,
        zoom_step,
        max_rotation,
        min_rotation,
        rotation_step,
        resetStates
      }}
    >
      {children}
    </ImageCropContext.Provider>
  );
};

export const useImageCropContext = () => useContext(ImageCropContext);

export default ImageCropProvider;

Enter fullscreen mode Exit fullscreen mode

In the provided code snippet, we've defined the ImageCropProvider component. This component serves as the context provider and wraps its children with the ImageCropContext.Provider. Let's take a closer look at how this works:

The ImageCropContext will be our centralized state management solution for image cropping and rotating.
It is a functional component that takes several optional props related to the configuration of image cropping and rotation. These props include max_zoom, min_zoom, zoom_step, max_rotation, min_rotation, and rotation_step, which can be customized based on the specific requirements of the application.

It will encapsulate the following state variables:

  • image: Holds the selected image that we want to crop and rotate.
  • crop: Represents the x and y coordinates of the current cropping area.
  • rotation: Keeps track of the rotation angle of the image.
  • zoom: Manages the zoom level of the image.
  • croppedAreaPixels: Stores the pixel values of the cropped area.

Additionally, we'll include several utility functions that allow us to manipulate the zoom and rotation of the image:

  • handleZoomIn: Increases the zoom level (up to a maximum value).
  • handleZoomOut: Decreases the zoom level (down to a minimum value).
  • handleRotateCw: Rotates the image clockwise by a specified angle.
  • handleRotateAntiCw: Rotates the image anti-clockwise by a specified angle.
  • resetStates: Reset all the states.

Furthermore, we've created a function called getProcessedImage. This function uses a helper function, getCroppedImg (not shown in this code snippet), to extract the cropped image based on the original image, the pixel values of the cropped area, and the rotation angle. The result is a new File object representing the cropped image, which can be used later to upload to the server.

Also we created a custom hook useImageCropContext to consume the ImageCropContext in other components

Building the Custom Cropper Component

Now, it's time to build a custom Cropper component that utilizes the react-easy-crop library, allowing users to interactively crop and rotate their selected images.

//src/components/cropper/Cropper.jsx

import EasyCropper from 'react-easy-crop';
import { useImageCropContext } from '../../providers/ImageCropProvider';

const Cropper = () => {
  const { image, zoom, setZoom, rotation, setRotation, crop, setCrop, onCropComplete } =
    useImageCropContext();

  return (
    <EasyCropper
      image={image || undefined}
      crop={crop}
      zoom={zoom}
      rotation={rotation}
      cropShape="round"
      aspect={1}
      onCropChange={setCrop}
      onCropComplete={onCropComplete}
      onZoomChange={setZoom}
      setRotation={setRotation}
      showGrid={false}
      cropSize={{ width: 185, height: 185 }}
      style={{
        containerStyle: {
          height: 220,
          width: 220,
          top: 8,
          bottom: 8,
          left: 8,
          right: 8
        }
      }}
    />
  );
};

export default Cropper;

Enter fullscreen mode Exit fullscreen mode

The Cropper component serves as an interface between the react-easy-crop library and our ImageCropContext. It accesses the necessary state and functions from the context using the useImageCropContext custom hook. Here's how it works:

  1. Accessing Context Data:
    • We use the useImageCropContext hook to access the data from the ImageCropContext. This hook returns an object that contains the state variables and functions we need for the cropping and rotating functionality.
    • Destructuring this object, we get access to image, zoom, setZoom, rotation, setRotation, crop, setCrop, and onCropComplete.
  2. EasyCropper Component:
    • The EasyCropper component is imported from the react-easy-crop library. It is a versatile component that provides interactive image cropping and rotation features.
    • We pass various props to this component to control its behavior based on the state from the ImageCropContext.
  3. Props and Functionality:
    • image: We provide the image from our context as a prop to EasyCropper so that it knows which image to crop and display. If image is undefined, the cropper will not render any image initially.
    • crop, zoom, and rotation: These props are linked to their respective state variables from the context, ensuring that the cropper reflects the current crop, zoom, and rotation values.
    • cropShape and aspect: We set cropShape to "round" and aspect to 1, indicating that the cropping area should have a circular shape and maintain a 1:1 aspect ratio.
    • onCropChange, onZoomChange, and setRotation: These callback props update the corresponding state variables in the context whenever the user interacts with the cropper.
  4. Styling the Cropper:
    • We provide a style object to customize the appearance of the EasyCropper component. In this example, we set the height and width of the cropper container to 220 pixels, with a margin of 8 pixels around the edges to create some padding.

Building the Image Crop Modal Content

With the ImageCropContext and Cropper component in place, we're ready to construct the core of our image crop modal. The ImageCropModalContent component is where users can interact with the cropper, adjust zoom and rotation, and upload new images for cropping and rotating.

To begin, we'll need some CSS styles for the crop container. Add the following CSS in the index.css file:

.crop-container {
  position: relative;
  width: 236px;
  height: 236px;

  background: linear-gradient(to right, #cbd4e1 8px, transparent 8px) 0 0,
    linear-gradient(to right, #cbd4e1 8px, transparent 8px) 0 100%,
    linear-gradient(to left, #cbd4e1 8px, transparent 8px) 100% 0,
    linear-gradient(to left, #cbd4e1 8px, transparent 8px) 100% 100%,
    linear-gradient(to bottom, #cbd4e1 8px, transparent 8px) 0 0,
    linear-gradient(to bottom, #cbd4e1 8px, transparent 8px) 100% 0,
    linear-gradient(to top, #cbd4e1 8px, transparent 8px) 0 100%,
    linear-gradient(to top, #cbd4e1 8px, transparent 8px) 100% 100%;

  background-repeat: no-repeat;
  background-size: 70px 70px;
}
.reactEasyCrop_CropArea {
  color: rgba(255, 255, 255, 0.8) !important;
}

Enter fullscreen mode Exit fullscreen mode

Now, let's proceed with the ImageCropModalContent component implementation:

//src/components/ImageCropModalContent.jsx

import { readFile } from '../helpers/cropImage';
import { useImageCropContext } from '../providers/ImageCropProvider';
import Button from '../components/base/Button';
import Cropper from '../components/cropper/Cropper';
import { RotationSlider, ZoomSlider } from '../components/cropper/Sliders';

const ImageCropModalContent = ({ handleDone, handleClose }) => {
  const { setImage } = useImageCropContext();

  const handleFileChange = async ({ target: { files } }) => {
    const file = files && files[0];
    const imageDataUrl = await readFile(file);
    setImage(imageDataUrl);
  };

  return (
    <div className="text-center relative">
      <h5 className="text-gray-800 mb-4">Edit profile picture</h5>
      <div className="border border-dashed border-gray-200 p-6 rounded-lg">
        <div className="flex justify-center">
          <div className="crop-container mb-4">
            <Cropper />
          </div>
        </div>
        <ZoomSlider className="mb-4" />
        <RotationSlider className="mb-4" />
        <input
          type="file"
          multiple
          onChange={handleFileChange}
          className="hidden"
          id="avatarInput"
          accept="image/*"
        />

        <Button variant="light" className="shadow w-full mb-4 hover:shadow-lg">
          <label htmlFor="avatarInput">Upload Another Picture</label>
        </Button>
        <div className="flex gap-2">
          <Button variant="secondary" onClick={handleClose}>
            Cancel
          </Button>
          <Button variant="primary" className="w-full" onClick={handleDone}>
            Done & Save
          </Button>
        </div>
      </div>
    </div>
  );
};

export default ImageCropModalContent;

Enter fullscreen mode Exit fullscreen mode

Let's explore the code of the ImageCropModalContent component and the ZoomSlider and RotationSlider components, which enable users to control the zoom and rotation of the image while cropping.

  • The ImageCropModalContent component begins by importing the readFile function from helpers/cropImage. This function is responsible for reading the image file and returning its data URL.
  • We also import the useImageCropContext hook from providers/ImageCropProvider to access the setImage function from the context. When users select an image file, the handleFileChange function reads the image data URL and sets it in the context using setImage.
  • Rendering the Cropper: The ImageCropModalContent component renders the Cropper component, allowing users to interactively crop and rotate the selected image. We place the Cropper inside a container with the class crop-container for styling purposes.
  • Zoom and Rotation Sliders: We import the ZoomSlider and RotationSlider components from components/cropper/Sliders. These sliders are responsible for adjusting the zoom level and rotation angle of the image, respectively.
  • File Input and Upload Button: We include an input element with type file to allow users to upload another picture for cropping. The handleFileChange function is triggered when users select a new image file. When users click on the Upload Another Picture button, it triggers the file input to open, enabling them to choose a new image.
  • Action Buttons: We provide two action buttons: Cancel and Done & Save. The Cancel button closes the image crop modal when clicked, while the Done & Save button saves the cropped image and triggers the handleDone function, which is passed as a prop from the parent component.

ZoomSlider and RotationSlider Components:

The ZoomSlider and RotationSlider components allow users to control the zoom level and rotation angle of the image during cropping. They interact with the useImageCropContext hook to access the necessary state and functions from the ImageCropContext.

//src/components/cropper/Sliders.jsx

import { useImageCropContext } from '../../providers/ImageCropProvider';
import {
  ArrowUturnLeftIcon,
  ArrowUturnRightIcon,
  MinusIcon,
  PlusIcon
} from '@heroicons/react/24/solid';
import classNames from 'classnames';

export const ZoomSlider = ({ className }) => {
  const { zoom, setZoom, handleZoomIn, handleZoomOut, max_zoom, min_zoom, zoom_step } =
    useImageCropContext();

  return (
    <div className={classNames(className, 'flex items-center justify-center gap-2')}>
      <button className="p-1" onClick={handleZoomOut}>
        <MinusIcon className="text-gray-400 w-4" />
      </button>
      <input
        type="range"
        name="volju"
        min={min_zoom}
        max={max_zoom}
        step={zoom_step}
        value={zoom}
        onChange={e => {
          setZoom(Number(e.target.value));
        }}
      />
      <button className="p-1" onClick={handleZoomIn}>
        <PlusIcon className="text-gray-400 w-4" />
      </button>
    </div>
  );
};

export const RotationSlider = ({ className }) => {
  const {
    rotation,
    setRotation,
    max_rotation,
    min_rotation,
    rotation_step,
    handleRotateAntiCw,
    handleRotateCw
  } = useImageCropContext();

  return (
    <div className={classNames(className, 'flex items-center justify-center gap-2')}>
      <button className="p-1" onClick={handleRotateAntiCw}>
        <ArrowUturnLeftIcon className="text-gray-400 w-4" />
      </button>
      <input
        type="range"
        name="volju"
        min={min_rotation}
        max={max_rotation}
        step={rotation_step}
        value={rotation}
        onChange={e => {
          setRotation(Number(e.target.value));
        }}
      />
      <button className="p-1" onClick={handleRotateCw}>
        <ArrowUturnRightIcon className="text-gray-400 w-4" />
      </button>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Helper Functions for Image Processing:

To achieve image cropping and rotating, we utilize a set of helper functions adapted from the examples of the react-easy-crop library. These functions simplify the image processing tasks and enable us to generate the final cropped image.

//src/helpers/cropImage.js

export const readFile = file => {
  return new Promise(resolve => {
    const reader = new FileReader();
    reader.addEventListener('load', () => resolve(reader.result), false);
    reader.readAsDataURL(file);
  });
};

export 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'); // needed to avoid cross-origin issues on CodeSandbox
    image.src = url;
  });

export function getRadianAngle(degreeValue) {
  return (degreeValue * Math.PI) / 180;
}

/**
 * Returns the new bounding area of a rotated rectangle.
 */
export function rotateSize(width, height, rotation) {
  const rotRad = getRadianAngle(rotation);

  return {
    width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
    height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height)
  };
}

const getCroppedImg = async (
  imageSrc,
  pixelCrop = { x: 0, y: 0 },
  rotation = 0,
  flip = { horizontal: false, vertical: false }
) => {
  const image = await createImage(imageSrc);
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  if (!ctx) {
    return null;
  }

  const rotRad = getRadianAngle(rotation);

  // calculate bounding box of the rotated image
  const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);

  // set canvas size to match the bounding box
  canvas.width = bBoxWidth;
  canvas.height = bBoxHeight;

  // translate canvas context to a central location to allow rotating and flipping around the center
  ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
  ctx.rotate(rotRad);
  ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
  ctx.translate(-image.width / 2, -image.height / 2);

  // draw rotated image
  ctx.drawImage(image, 0, 0);

  // croppedAreaPixels values are bounding box relative
  // extract the cropped image using these values
  const data = ctx.getImageData(pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height);

  // 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 at the top left corner
  ctx.putImageData(data, 0, 0);

  // As Base64 string
  // return canvas.toDataURL('image/jpeg');

  // As a blob
  return new Promise(resolve => {
    canvas.toBlob(file => {
      resolve({ file, url: URL.createObjectURL(file) });
    }, 'image/jpeg');
  });
};

export default getCroppedImg;

Enter fullscreen mode Exit fullscreen mode

We used readFile, getCroppedImg functions in our components. Others functions wil be user internally among this functions.

Implementing the ImageModal and ImageCrop Interaction

In this section, we'll create the ImageCrop component, which acts as the main entry point for users to interact with the image modal and crop/rotate their chosen images.

//src/components/ImageCrop.jsx

import { useState } from 'react';
import user1 from '../assets/user_1.png';
import Modal from '../components/base/Modal';
import { readFile } from '../helpers/cropImage';
import ImageCropModalContent from './ImageCropModalContent';
import { useImageCropContext } from '../providers/ImageCropProvider';

const ImageCrop = () => {
  const [openModal, setOpenModal] = useState(false);
  const [preview, setPreview] = useState(user1);

  const { getProcessedImage, setImage, resetStates } = useImageCropContext();

  const handleDone = async () => {
    const avatar = await getProcessedImage();
    setPreview(window.URL.createObjectURL(avatar));
    resetStates();
    setOpenModal(false);
  };

  const handleFileChange = async ({ target: { files } }) => {
    const file = files && files[0];
    const imageDataUrl = await readFile(file);
    setImage(imageDataUrl);
    setOpenModal(true);
  };

  return (
    <div className="bg-gray-100 h-screen flex justify-center items-center">
      <input
        type="file"
        onChange={handleFileChange}
        className="hidden"
        id="avatarInput"
        accept="image/*"
      />
      <label htmlFor="avatarInput" className="cursor-pointer">
        <img
          src={preview}
          height={192}
          width={192}
          className="object-cover rounded-full h-48 w-48"
          alt=""
        />
      </label>

      <Modal open={openModal} handleClose={() => setOpenModal(false)}>
        <ImageCropModalContent handleDone={handleDone} handleClose={() => setOpenModal(false)} />
      </Modal>
    </div>
  );
};

export default ImageCrop;

Enter fullscreen mode Exit fullscreen mode

Integrating with ImageCropContext:

  • State and Image Preview: The ImageCrop component sets up two state variables: openModal and preview. openModal: Controls the visibility of the image modal. It is initially set to false and becomes true when users upload an image or open the modal to crop and rotate. preview: Holds the URL of the image to be displayed as a preview before cropping. It is initially set to a default image (user1 in this case).
  • Image Upload and Modal Interaction: Users can click on the img element, which is a label for the hidden file input. When clicked, the file input dialog opens, allowing users to select an image to upload. The handleUpload function is triggered when an image is selected. It reads the image data URL using the readFile function and sets it in the context using setImage. It also sets the openModal state to true, showing the image modal.
  • ImageModal and ImageCropModalContent Integration: The ImageCrop component renders an ImageModal component. The ImageCropModalContent is passed as the content of the modal. The open prop of Modal controls the visibility of the modal, and the handleClose function passed as the handleClose prop is used to close the modal when needed.
  • Image Cropping and Preview Update: The handleDone function is called when users finish cropping and click the Done & Save button in the ImageCropModalContent. Inside handleDone, we call the getCroppedImage function to get the cropped image from the context. The resulting File object is converted into a preview URL using URL.createObjectURL, which is then set in the preview state, updating the image preview with the cropped version. Finally, we set openModal to false, closing the image modal.

Integrating the ImageCrop Component

The App component serves as the container for the ImageCrop component, which provides users with an interface to interact with the image modal for cropping and rotating images. In your case it may be a different component

//src/App.jsx

import ImageCrop from './components/ImageCrop';
import ImageCropProvider from './providers/ImageCropProvider';

const App = () => {
  return (
    <div className="bg-gray-100 h-screen flex justify-center items-center">
      <ImageCropProvider>
        <ImageCrop />
      </ImageCropProvider>
    </div>
  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

In the App component, we wrap the ImageCrop component with the ImageCropProvider. This ensures that the ImageCrop component has access to the ImageCropContext, enabling it to manage the state and functionalities for image cropping and rotation effectively.
The ImageCrop component serves as the main user interaction point for selecting, cropping, and rotating images.
By wrapping the ImageCrop component with the ImageCropProvider, it can access the necessary context and seamlessly interact with the Cropper component and ImageCropModalContent.

Conclusion

In conclusion, we have successfully implemented an image modal with crop and rotation functionalities using React, and the react-easy-crop library. By utilizing the ImageCropProvider context, we effectively managed the state and actions required for image manipulation, reducing the need for excessive prop passing between components.

With this image modal in place, users can now effortlessly edit their profile pictures or any other images uploaded to the application.

here is full project code link: GitHub Code Source

Hope this tutorial has been helpful in expanding your knowledge of React, and that you're inspired to apply these techniques to enhance your own projects.

Top comments (2)

Collapse
 
philipokorie profile image
Philip Okorie

Hey, I am a little lost in why the image crop provider has to wrap around the image crop. The modal just remains open and has no way to close

Collapse
 
mizanrifat profile image
Mizan-Rifat

Share your code...