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.
- Creando el componente de titulo.
- Creando el componente de Drag & Drop.
- Creando el componente de Box Drag & Drop.
- Creando el componente de Image Selected.
- Llenando el componente con funciones y estado.
- Mostrar link de la imagen subida a Cloudinary.
- Ocultar link de la imagen después de unos segundos.
- Introducción.
🟣 Primeros pasos.
🟠 Configurando Cloudinary.
- Iniciar sesión en Cloudinary o crearte una cuenta.
- 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).
- Presionar el icono de engrane que te llevara a la configuración.
- Seleccionar la pestaña upload.
- Bajar hasta donde dice “Upload presets:”
- Presionar el enlace que dice “Add upload preset”
- 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)
- Donde dice “Signing Mode” seleccionar Unsigned
- 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
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 .
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
🟣 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>
)
}
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
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>
</>
)
}
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>
🟣 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>
)
}
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>
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}/>
🟣 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>
)
}
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>
🟣 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>
</>
)
}
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 }} />
🟠 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> => {};
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;
}
};
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);
};
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;
}
};
Ahora sí, es hora de usar la función que acabamos de crear.
- Primero colocamos el loading en true.
- 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).
- Luego colocamos el loading en false.
- Evaluamos si la URL no es null.
- Si No es nula, actualizamos el estado y guardamos esa URL.
- Si es nula, mandamos una alerta de Error.
- 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([]);
}
🟣 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>
}
</>
)
}
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>
</>
)
🟣 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])
🟣 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
}
}
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>
</>
)
}
Gracias por llegar hasta aquí!👐👐
Te dejo el código por si lo quieres revisar! ⬇️
Top comments (4)
Excelente post en Español, bro!
Gracias bro! 🙌
Excelente post en Español, me ayudo mucho!
Gracias!