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.
you'll need to add the
multipleparameter 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