Reputation: 21
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
"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
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
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
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
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