reactjs - No se puede subir múltiples imágenes en Cloudinary
Quiero crear una página de productos en la que pueda subir múltiples imágenes usando Cloudinary en Next. Aquí creé un componente para subir imágenes
Componente de carga de imágenes
"use client";
import { CldUploadWidget } from 'next-cloudinary';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import Image from 'next/image';
import { ImagePlus, Trash } from 'lucide-react';
interface ImageUploadProps {
disabled?: boolean;
onChange: (value: string) => void;
onRemove: (value: string) => void;
value: string[];
}
const ImageUpload: React.FC<ImageUploadProps> = ({
disabled,
onChange,
onRemove,
value
}) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const onUpload = (result: any) => {
onChange(result.info.secure_url);
};
if (!isMounted) {
return null;
}
return (
<div>
<div className="mb-4 flex items-center gap-4">
{value.map((url) => (
<div key={url} className="relative w-[200px] h-[200px] rounded-md overflow-hidden">
<div className="z-10 absolute top-2 right-2">
<Button type="button" onClick={() => onRemove(url)} variant="destructive" size="sm">
<Trash className="h-4 w-4" />
</Button>
</div>
<Image
fill
sizes=''
className="object-cover"
alt="Image"
src={url}
/>
</div>
))
}
</div>
<CldUploadWidget onSuccess={onUpload} uploadPreset="ox48luzl">
{({ open }) => {
const onClick = () => {
open();
};
return (
<Button
type="button"
disabled={disabled}
variant="secondary"
onClick={onClick}
>
<ImagePlus className="h-4 w-4 mr-2" />
Upload an Image
</Button>
);
}}
</CldUploadWidget>
</div>
);
}
export default ImageUpload;
y
Página del Formulario de Producto
donde llamo a mi carga de imagen
'use client'
import { Button } from "@/components/ui/button"
import { Heading } from "@/components/ui/heading"
import { Product, Image, Category } from "@prisma/client";
import { Trash } from "lucide-react"
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form";
import * as z from "zod"
import { AlertModal } from "@/components/modals/alert-model";
import axios from "axios";
import toast from "react-hot-toast";
import { Separator } from "@/components/ui/separator";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import ImageUpload from "@/components/ui/image-upload";
const formSchema = z.object({
name: z.string().min(1),
promocode: z.string().min(2),
affiliateLink: z.string().min(1),
description: z.string().min(1),
images:z.object({url:z.string()}).array(),
categoryId: z.string().min(1),
price: z.coerce.number().min(1),
})
type ProductFormValues = z.infer<typeof formSchema>;
interface ProductFormProps {
initialData: Product & {
images: Image[]
} | null;
categories: Category[]
};
const ProductForm: React.FC<ProductFormProps> = ({
initialData,
categories
}) => {
const params = useParams();
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const title = initialData ? 'Edit Product' : 'Create Product'
const description = initialData ? 'Edit a Product' : 'Add a new Product';
const toastMassege = initialData ? 'Product Update' : 'Product Created';
const action = initialData ? 'Save Changes' : 'Create';
const defaultValues = initialData ? {
...initialData,
price: parseFloat(String(initialData?.price)),
promocode: initialData.promocode || "",
} : {
name: '',
images:[],
price:0,
description: '',
catogoryId: '',
promocode: '',
affiliateLink: '',
}
const form = useForm<ProductFormValues>({
resolver: zodResolver(formSchema),defaultValues
})
const onDelete = async () => {
try {
setLoading(true)
await axios.delete(`/api/products/${params.productId}`)
router.push('/products')
toast.success('Product Deleted Successfully!')
} catch (error: any) {
toast.error('something wen wrong')
}
finally {
setLoading(false)
}
}
return (
<>
<AlertModal
isOpen={open}
onClose={() => setOpen(true)}
onConfirm={onDelete}
loading={loading}
/>
<div className="flex item-center justify-between">
<Heading title={title} description={description} />
{initialData &&(
<Button
disabled={loading}
variant="destructive"
size="sm"
>
<Trash className="h-4 w-4" />
</Button>
)}
</div>
<Separator/>
<Form {...form}>
<form className="space-y-8 w-full">
<FormField
control={form.control}
name="images"
render={({ field }) => (
<FormItem>
<FormLabel>Images</FormLabel>
<FormControl>
<ImageUpload
value={field.value.map((image)=>image.url)}
disabled={loading}
onChange={(url) => field.onChange([...field.value, { url }])}
onRemove={(url) => field.onChange([...field.value.filter((current) => current.url !== url)])}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({field})=>(
<FormItem>
<FormLabel>Product Name</FormLabel>
<FormControl>
<Input disabled={loading} placeholder="Enter Product Name" {...field}/>
</FormControl>
</FormItem>
)} />
</form>
</Form>
</>
)
}
export default ProductForm
En este código, casi todo funciona bien, pero al intentar subir varias imágenes en el widget de Cloudinary, solo se muestra la primera o la primera imagen subida y se almacena en el valor.
IU quería implementar un conjunto de URL de imágenes subidas y almacenadas.
Respuesta #1
necesitarás agregar el parámetro multiple al widget de carga.
Puedes leer sobre ello en la documentación: https://cloudinary.com/documentation/upload_widget_reference#:~:text=opened.%0ADefault%3A%20local-,multiple,-Boolean
Respuesta #2
Prueba esto...
onChange={(url) => { const currentImages = form.getValues("images"); const newImage = { url: url }; const updatedImages = [...currentImages, newImage]; form.setValue("images", updatedImages, { shouldValidate: true }); console.log("Updated images:", updatedImages); }}
Al usar el widget de carga de Cloudinary con React Hook Form, la función onChange se ejecuta para cada imagen individualmente, por lo que si estás subiendo múltiples imágenes, onChange se llama varias veces, una por cada imagen. Esto requiere capturar la URL de cada imagen y agregarla al campo de formulario de imágenes sin sobrescribir las imágenes anteriores. Así que tienes que capturar las URLs y agregarlas directamente en el campo de formulario de imágenes.
