Búsqueda de sitios web

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.