Upload Image App
Aplicacion para subir imagenes a la nube de Cloudinary, usando Drag & Drop. 📤
Tecnologias usadas
- React JS
- Create React App
- TypeScript
- CSS vanilla
- Cloudinary API
Instalación
npm install
Correr la aplicación
npm start
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
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
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.
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:
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[0]
).
<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
{({
imageList,
onImageUpload,
dragProps,
isDragging,
onImageRemove,
onImageUpdate,
}) => (
)}
</ImageUploading>
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}/>
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:
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:
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>
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 }} />
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.
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([]);
}
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>
</>
)
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])
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! ⬇️