Crear una aplicación para subir images - React y Cloudinary ⚛️.

Franklin Martinez - Jun 9 '22 - - Dev Community

Esta aplicación consiste en una interfaz donde se podrán subir imágenes mediante Drag & Drop y dicha imagen se guardara en Cloudinary.

El enlace al código esta al final de este post.

Índice


🟣 Primeros pasos.

🟠 Configurando Cloudinary.

  1. Iniciar sesión en Cloudinary o crearte una cuenta.
  2. En el Dashboard, te aparcera el nombre de tu nube (deberás guardarlo en un bloc de notas o algo ya que lo usaremos después).
  3. Presionar el icono de engrane que te llevara a la configuración.
  4. Seleccionar la pestaña upload.
  5. Bajar hasta donde dice “Upload presets:”
  6. Presionar el enlace que dice “Add upload preset”
  7. Donde dice “Upload preset name”, en la caja de texto colocar un nombre para ese preset. (ej: zt1zhk4z, deberás guardarlo en un bloc de notas o algo ya que lo usaremos después)
  8. Donde dice “Signing Mode” seleccionar Unsigned
  9. Presionar el botón save (se encuentra en la parte superior de la pagina) para guardar preset.

🟠 Creando el proyecto con create-react-app.

Necesitamos crear un nuevo proyecto de React. En este caso lo hare con la herramienta de create-react-app usando TypeScript.



npx create-react-app upload-image-app --template typescript


Enter fullscreen mode Exit fullscreen mode

Luego de haberse creado nos dirigimos al proyecto lo abrimos con el editor de cogido de preferencia. En mi caso, Visual Studio Code.



cd upload-image-app && code .


Enter fullscreen mode Exit fullscreen mode

Ahora, necesitaremos instalar un paquete de terceros llamado react-images-uploading, el cual nos ayudara a trabajar la acción de Drag & Drop con las imágenes,



npm install react-images-uploading


Enter fullscreen mode Exit fullscreen mode

🟣 Creando el componente de titulo.

Dentro de la carpeta src/components creamos el archivo Title.tsx. Y agregamos el siguiente código.



import React from 'react';

export const Title = () => {
    return (
        <>
        <div className='container_blob'>
            <SVG/>
        </div>
        <h1 className="title">
            <span>Upload image</span><br />
            <span> with</span> <br />
            <span>React & Cloudinary</span>
        </h1>
        </>
    )
}

const SVG = () => {
    return (
        <svg className='svg_blob' viewBox="50 0 100 100" xmlns="http://www.w3.org/2000/svg">
            <path d="M29.9,-47.6C39.2,-34.4,47.5,-26.3,49.9,-16.8C52.2,-7.2,48.5,3.7,44.7,14.3C40.9,24.9,37,35.2,29.5,44.4C22,53.6,11,61.8,-1.3,63.5C-13.6,65.3,-27.1,60.6,-39.3,52.9C-51.5,45.2,-62.2,34.5,-66.6,21.5C-71,8.5,-69,-6.6,-62.9,-18.9C-56.8,-31.1,-46.5,-40.5,-35.3,-53C-24.1,-65.6,-12.1,-81.3,-0.9,-80C10.3,-78.8,20.6,-60.7,29.9,-47.6Z" transform="translate(100 100)" />
        </svg>
    )
}


Enter fullscreen mode Exit fullscreen mode

Después nos dirigimos al archivo src/App.tsx y borramos todo, para agregar lo siguiente:



import React from 'react';
import { Title } from './components';

const App = () => {
  return (
    <div className="container-grid">
      <Title />
    </div>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

Title component

Para la parte de los estilos, pueden revisar mi código que esta en GitHub, esto lo hago para que el articulo no se haga tan largo y solo concentrarme en la parte importante.

🟣 Creando el componente de Drag & Drop.

Dentro de la carpeta src/components creamos un archivo llamado DragAndDrop.tsx

Primero haremos uso del estado para manejar el comportamiento del componente cuando se seleccione alguna imagen o se arrastre y suelte la imagen dentro del componente.

El componente ImageUploading le colocamos los siguientes propiedades:

  • multiple → en false, para solo seleccionar una imagen a la vez.
  • maxNumber → en 1, ya que solo aceptaremos una imagen.
  • value → un valor de tipo ImageListType. Le pasamos el valor del estado “images”.
  • onChange → un método que se ejecuta cuando cuando se selecciona una imagen (este método recibe dos parámetros, pero a nosotros solo nos importa el primero, el cual es un array de objetos que contiene la información de la imagen seleccionada). Le pasamos la función handleChange (dicha función actualiza el estado, agregando la imagen seleccionada al estado).


import React, { useState } from 'react';
import ImageUploading, { ImageListType } from "react-images-uploading";

export const DragAndDrop = () => {

  const [images, setImages] = useState<ImageListType>([]);

  const handleChange = (imageList: ImageListType) => setImages(imageList);

  return (
    <>      
      <ImageUploading multiple={false} maxNumber={1} value={images} onChange={handleChange}>
      </ImageUploading>
    </>
  )
}


Enter fullscreen mode Exit fullscreen mode

El componente ImageUploading recibe un función como hijo, dicha función nos da acceso a ciertas parámetros, de los cuales usaremos los siguientes:

  • ImageList → un valor de tipo ImageListType, que nos trae un arreglo de las imágenes que se han seleccionado (en este caso solo debe ser una imagen seleccionada, por lo cual siempre apuntaremos a la posición 0, ejemplo: imageList[0]).
  • dragProps → es un conjunto de métodos que nos ayudaran a realizar la acción de Drag & Drop.
  • isDragging → retorna true si se esta arrastrando una imagen al componente, de lo contrario se queda en false.
  • onImageUpload → Método que al ejecutarse abre el explorador de archivos del dispositivo para seleccionar una imagen.
  • onImageRemove → Método que recibe un índice de la imagen que se quiere quitar y la remueve de la lista (que en este caso siempre sera el índice 0).
  • onImageUpdate → Método que recibe un índice de la imagen que se quiere actualizar (que en este caso siempre sera el índice 0)., y abre el explorador de archivos para seleccionar una nueva imagen.


<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
    {({
        imageList,
        onImageUpload,
        dragProps,
        isDragging,
        onImageRemove,
        onImageUpdate,
    }) => (

    )}
</ImageUploading>


Enter fullscreen mode Exit fullscreen mode

🟣 Creando el componente de Box Drag & Drop.

La función dentro del componente <ImageUploading/> debe retornar JSX

Dentro de la carpeta src/components creamos un archivo llamado BoxDragAndDrop.tsx

Este componente es donde se hará el drag & drop o se dará click para seleccionar alguna imagen

Agregamos el siguiente código:



import React from 'react';

interface Props{
  onImageUpload: () => void;
  dragProps: any;
  isDragging: boolean
}

export const BoxDragAndDrop = ({ isDragging, onImageUpload, dragProps }:Props) => {
    return (
      <div
        onClick={onImageUpload}
        {...dragProps}
        className={`container-dnd center-flex ${isDragging ? 'isDragging' : ''}`}
      >
        <span className='label-dnd'>Chosee an Image or Drag and Drop an Image 📤</span>
      </div>
    )
  }


Enter fullscreen mode Exit fullscreen mode

Luego agregamos el componente BoxDragAndDrop.tsx en el componente DragAndDrop.tsx

Dentro de la función haremos una condicional, dependiendo de la lista de imágenes, si esta vacía debe mostrar el componente BoxDragAndDrop.tsx sino significa que ya hay una imagen seleccionada y debe mostrar dicha imagen.



<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
  {({
    imageList,
    onImageUpload,
    dragProps,
    isDragging,
    onImageRemove,
    onImageUpdate,
  }) => (
    <>
      {
        imageList[0]
          ? <p>SELECTED IMAGE</p>
          : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
      }
    </>
  )}
</ImageUploading>


Enter fullscreen mode Exit fullscreen mode

En el componente BoxDragAndDrop.tsx se nota tal vez una sintaxis rara, es una forma diferente de pasar propiedades, solo lo hice para ahorrar un par de lineas. Aunque, si es difícil de leer puedes optar por la otra forma.



<BoxDragAndDrop dragProps={dragProps} isDragging={isDragging} onImageUpload={onImageUpload}/>


Enter fullscreen mode Exit fullscreen mode

Box component

🟣 Creando el componente de Image Selected.

Dentro de la carpeta src/components creamos un archivo llamado ImageSelected.tsx

Este componente mostrara la imagen que se ha seleccionado, así como 3 botones los cuales servirán para:

  • Subir la imagen a Cloudinary
  • Remover la imagen seleccionada
  • Actualizar la imagen seleccionada.

Agregamos el siguiente código:



import React from 'react';

interface Props {
  loading: boolean;
  img: string;
  onUpload: () => Promise<void>;
  onImageRemove: (index: number) => void;
  onImageUpdate: (index: number) => void
}

export const ImageSelected = ({ 
    img, 
    loading, 
    onUpload, 
    onImageRemove, 
    onImageUpdate 
}: Props) => {

  return (
    <div>
      <img className='image-selected' src={img} alt='image-selected' width={300} />
      <div className='container-buttons'>
        {
          loading
            ? <p className='loading-label'>Upload image ⏳...</p>
            : <>
              <button disabled={loading} onClick={onUpload}>Upload 📤</button>
              <button disabled={loading} onClick={() => onImageUpdate(0)}>Update ✏️</button>
              <button disabled={loading} onClick={() => onImageRemove(0)}>Cancel ❌</button>
            </>
        }
      </div>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

Este componente recibe 5 parámetros:

  • img → la imagen seleccionada que se mostrará en pantalla
  • loading → valor booleano que indicara cuando la imagen este siendo cargada a Cloudinary.
  • onUpload → Método el cual se encargara de subir la imagen a Cloudinary (se explica más a detalle a continuación)
  • onImageRemove
  • onImageUpdate

Luego agregamos el componente ImageSelected.tsx en el componente DragAndDrop.tsx

Les marcara error, ya que le faltan los parámetros que son obligatorios, por lo que los tenemos que crear.



<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
  {({
    imageList,
    onImageUpload,
    dragProps,
    isDragging,
    onImageRemove,
    onImageUpdate,
  }) => (
    <>
      {
        imageList[0]
          ? <ImageSelected  />
          : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
      }
    </>
  )}
    </ImageUploading>


Enter fullscreen mode Exit fullscreen mode

Image Selected Component

🟣 Llenando el componente con funciones y estado.

En el componente DragAndDrop.tsx necesitaremos agregar un nuevo estado para manejar el loading y otro estado para agregar la URL que la imagen ya guardada en cloudinary.

Agregamos la función onUpload, que por el momento no hace nada, aún.



export const DragAndDrop = () => {
  const [images, setImages] = useState<ImageListType>([]);
    const [urlImage, setUrlImage] = useState('')
  const [loading, setLoading] = useState(false);

  const handleChange = (imageList: ImageListType) => setImages(imageList);

    const onUpload = () => {}

  return (
    <>
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ?  <ImageSelected  />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )
}


Enter fullscreen mode Exit fullscreen mode

Después ya podemos pasarle los parámetros al componente <ImageSelected/>

El parámetro img se obtiene de la propiedad imageList en la posición 0 accediendo a la propiedad dataURL.



<ImageSelected img={imageList[0].dataURL!}  {...{ onImageRemove, onUpload, onImageUpdate, loading }} />


Enter fullscreen mode Exit fullscreen mode

🟠 Agregando la funcionalidad para subir imágenes a Cloudinary.

Antes de ir al método onUpload, debemos prepara la función para hacer la llamada a la API de cloudinary. Para ello creamos la carpeta src/utils y dentro creamos el archivo fileUpload.ts y agregamos lo siguiente:

Creamos la función asíncrona fileUpload que recibe una imagen de tipo File y retorna un string que sera la URL de la imagen o null.

Aquí haremos uso de los datos que configuramos en cloudinary anteriormente. (el nombre de la nube y el preestablecido).

Sera mejor colocar dichos valores en variables de entorno, ya que son delicadas.



/*
const cloud_name = process.env.REACT_APP_CLOUD_NAME;
const preset = process.env.REACT_APP_PRESET;
*/
const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {};


Enter fullscreen mode Exit fullscreen mode

Luego construimos la URL para hacer la llamada a la API.



const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);

    try {
        const res = await fetch(cloudinaryUrl, {
            method: 'POST',
            body: formData
        });

        if (!res.ok) return null;

        const data = await res.json();
        return data.secure_url;

    } catch (error) {
        return null;
    }
};


Enter fullscreen mode Exit fullscreen mode

Luego construimos la data que vamos enviar a la API, en este caso la imagen.



const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);
};


Enter fullscreen mode Exit fullscreen mode

Finalmente hacemos uso de la API fetch para hacer la petición y mandar la data.

Si la respuesta no es correcta retornamos null y si no retornamos la URL de la imagen.



const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);

    try {
        const res = await fetch(cloudinaryUrl, {
            method: 'POST',
            body: formData
        });

        if (!res.ok) return null;

        const data = await res.json();
        return data.secure_url;

    } catch (error) {
        return null;
    }
};


Enter fullscreen mode Exit fullscreen mode

Ahora sí, es hora de usar la función que acabamos de crear.

  1. Primero colocamos el loading en true.
  2. Hacemos la llamada a la función fileUpload y le mandamos el valor del estado (recordando que es un arreglo de ImageListType, así que accedemos a la posición 0 a la propiedad file).
  3. Luego colocamos el loading en false.
  4. Evaluamos si la URL no es null.
    1. Si No es nula, actualizamos el estado y guardamos esa URL.
    2. Si es nula, mandamos una alerta de Error.
  5. Finalmente, vaciamos el estado de las imagen seleccionada.


const onUpload = async () => {
  setLoading(true);
  const url = await fileUpload(images[0].file!);
  setLoading(false);

  if (url) setUrlImage(url);
  else alert('Error, please try again later. ❌')

  setImages([]);
}


Enter fullscreen mode Exit fullscreen mode

🟣 Mostrar link de la imagen subida a Cloudinary.

Dentro de la carpeta src/components creamos un archivo llamado Message.tsx

El cual recibe la URL de la imagen, que puede ser null o un string.



import React from 'react';

interface Props {
    urlImage: string | null
}

export const Message = ({ urlImage }: Props) => {
    return (
        <>
            {
                urlImage && <span className='url-cloudinary-sumbit'>
                    Your Image uploaded successfully! ✅ 
                                        <a target='_blank' href={urlImage}> View Image</a>
                </span>
            }
        </>
    )
}


Enter fullscreen mode Exit fullscreen mode

Luego agregamos el componente Message.tsx en el componente DragAndDrop.tsx y le pasamos el valor del estado de urlImage.



return (
    <>
      <Message urlImage={urlImage} />
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ? <ImageSelected  {...{ onImageRemove, onImageUpdate, onUpload, loading }} img={imageList[0].dataURL!} />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )


Enter fullscreen mode Exit fullscreen mode

Message Component

🟣 Ocultar link de la imagen después de unos segundos.

El en componente DragAndDrop.tsx agregaremos un efecto. Lo que hará es que, después de 5 segundos, pondrá el valor del estado de urlImage en string vacío, lo que hará que no se cree debido a la condicional.



useEffect(() => {
  let timeout: NodeJS.Timeout;

  if(urlImage){
    timeout = setTimeout(()=> {
      setUrlImage('')
    }, 5000)
  }

  return () => {
   clearTimeout(timeout);
  }
}, [urlImage])


Enter fullscreen mode Exit fullscreen mode

🟣 Refactorizando el componente Drag & Drop y creando un custom hook.

Hay demasiada lógica en el componente, la cual podemos colocar en un custom hook.

Para ello creamos la carpeta Dentro de la carpeta src/hooks

Dentro de esa carpeta creamos el archivo useUploadImage.ts y movemos la lógica dentro de este hook.



import {useEffect, useState} from 'react';
import { ImageListType } from "react-images-uploading";
import { fileUpload } from "../utils";

export const useUploadImage = () => {

    const [images, setImages] = useState<ImageListType>([]);
    const [loading, setLoading] = useState(false);
    const [urlImage, setUrlImage] = useState('')

    const handleChange = (imageList: ImageListType) => setImages(imageList);

    const onUpload = async () => {
      setLoading(true);
      const url = await fileUpload(images[0].file!);
      setLoading(false);

      if (url) setUrlImage(url);
      else alert('Error, please try again later. ❌')

      setImages([]);
    }

    useEffect(() => {
      let timeout: NodeJS.Timeout;
      if(urlImage){
        timeout = setTimeout(()=> {
          setUrlImage('')
        }, 5000)
      }

      return () => {
       clearTimeout(timeout);
      }
    }, [urlImage])

    return {
        loading,
        onUpload,
        handleChange,
        urlImage,
        images
    }
}


Enter fullscreen mode Exit fullscreen mode

Y de esta manera nos quedaría el componente DragAndDrop.tsx

Nota que al componente ImageSelected le quitamos las propiedades loading y onUpload. y le pasamos …rest.



import React from 'react';
import ImageUploading from "react-images-uploading";
import { useUploadImage } from '../hooks';

import { ImageSelected, BoxDragAndDrop, Message } from './';

export const DragAndDrop = () => {

  const { urlImage, handleChange, images, ...rest } = useUploadImage();

  return (
    <>
      <Message urlImage={urlImage} />
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ? <ImageSelected  {...{ onImageRemove, onImageUpdate, ...rest }} img={imageList[0].dataURL!} />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )
}


Enter fullscreen mode Exit fullscreen mode

Gracias por llegar hasta aquí!👐👐
Te dejo el código por si lo quieres revisar! ⬇️

GitHub logo Franklin361 / upload-image-app

Application to upload images to Cloudinary via Drag & Drop ⚛️

Upload Image App

Aplicacion para subir imagenes a la nube de Cloudinary, usando Drag & Drop. 📤

Image cover

Tecnologias usadas

  • React JS
  • Create React App
  • TypeScript
  • CSS vanilla
  • Cloudinary API

Instalación

npm install

Correr la aplicación

npm start
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .