Nilesh Kurrey
Nilesh Kurrey

Reputation: 21

Unable to upload multiple images in Cloudinary

I want to create a products page in which I can upload multiple images using Cloudinary in next. Here I created a component for uploading image

Image Upload component

"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;

and

Product Form page

where I call my image upload

'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

In this code, almost everything works fine but when trying to upload multiple images in the Cloudinary widget only the first or first uploaded image displays and is stored in the value.

IU wanted to implement an array of image URLs uploaded and stored.

Upvotes: 2

Views: 533

Answers (3)

nabeer_dar
nabeer_dar

Reputation: 1

In the Products Page, modify the onChange function. In the Image Upload Page, change onUpload to onSuccess.

onChange={(url) => {
    const currentImages = form.getValues("images"); // Retrieve current images
    const newImage = { url: url }; // Create a new image object with the URL
    const updatedImages = [...currentImages, newImage]; // Add the new image to the array
    form.setValue("images", updatedImages, { shouldValidate: true }); // Update the form with validation
    console.log("Updated images:", updatedImages); // Log the updated images array for debugging
}}

const onSuccess = (result: any) => {
    const imageUrl = result.info.secure_url;  // Correct property for the secure URL
    console.log("Uploaded Image URL:", imageUrl);  // Log the image URL for debugging
    onChange(imageUrl);  // Call the onChange function with the correct URL
};

<CldUploadWidget onSuccess={onSuccess} uploadPreset="your preset here">
    {/* Add your child components or upload button here */}
</CldUploadWidget>

Upvotes: 0

Suleman Mughal
Suleman Mughal

Reputation: 97

Try this...

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); }}

When using Cloudinary's upload widget with React Hook Form, the onChange function runs for each image individually, so if you're uploading multiple images, the onChange gets called multiple times—once for each image. This requires capturing each image's URL and appending it to the images form field without overwriting the previous images. So you have to catch the urls and append them directly in the form field of images

Upvotes: 1

PixelCook
PixelCook

Reputation: 81

you'll need to add the multiple parameter to the upload widget.

You can read about it via documentation: https://cloudinary.com/documentation/upload_widget_reference#:~:text=opened.%0ADefault%3A%20local-,multiple,-Boolean

Upvotes: 2

Related Questions