I want to create a form that uploads the images to Cloudinary and then gets the image's URL and parses it into Supabase database, but I only want to upload the images when I click the Publish button (right now the image will auto upload when I selected it).
So I wondering how I can create a function for that purpose. Please help! Thank you.
Here is my code:
'use client';
import { useState, useEffect } from 'react'
import { useRouter, redirect } from 'next/navigation';
import { useUser } from '@/contexts/AuthContext';
import { Database } from '@/db_types'
import Button from "@/components/shared/Button"
import Editor from "@/components/shared/Editor"
type Posts = Database['public']['Tables']['posts']['Row']
const initialState = {
title: "",
description: "",
slug: "",
images: "",
};
export default function AddPost() {
const { userDetails , supabase } = useUser();
const [loading, setLoading] = useState(true)
const [title, setTitle] = useState<Posts['title']>(null)
const [description, setDescription] = useState<Posts['description']>(null)
const [slug, setSlug] = useState<Posts['slug']>(null)
const [user_id] = useState<Posts['user_id']>(null)
const [images, setImages] = useState<Posts['images']>(null)
const [imageData, setImageData] = useState(initialState);
if (!userDetails) {
redirect('/login');
}
const uploadImage = async (e : any) => {
const reader = new FileReader();
reader.onloadend = async () => {
setLoading(true);
};
if (!e.target.files || e.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}
const files = e.target.files[0]
if (!files) return;
const data = new FormData();
data.append("file", files);
data.append("upload_preset", "c_tags");
const res = await fetch(`${process.env.NEXT_PUBLIC_CLOUDINARY_API}`,
{
method: "POST",
body: data,
}
);
const file = await res.json();
setImageData({ ...imageData, images: file.secure_url });
setLoading(false);
};
async function addPost({
title,
description,
slug,
images,
user_id,
}: {
title: Posts['title']
description: Posts['description']
slug: Posts['slug']
images: Posts['images']
user_id: Posts['user_id']
}) {
try {
setLoading(true)
const updates = {
title,
description,
slug,
images : `${imageData.images}`,
created_at: new Date().toISOString(),
user_id: userDetails?.id
}
let { error } = await supabase.from('posts').upsert(updates)
if (error) throw error
alert('Published!')
} catch (error) {
alert('Error updating the data!')
console.log(error)
} finally {
setLoading(false)
}
}
return (
<div className="">
<div>
<form
className="mt-3"
onSubmit={(e) => {
e.preventDefault();
addPost({ title, description, slug, images, user_id })
}}
>
<input
id="images"
type="file"
className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
accept="image/*"
onChange={uploadImage}
/>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
value={title || ''}
onChange={(e) => setTitle(e.target.value)}
/>
<label htmlFor="Description">Content</label>
<Editor
description={description}
setDescription={setDescription}
/>
<label htmlFor="slug">Slug</label>
<input
id="slug"
type="text"
className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
value={slug || ''}
onChange={(e) => setSlug(e.target.value)}
/>
<div>
<Button
className="mt-5 bg-red-500"
onClick={() => addPost({ title, description, slug, user_id, images })}
>
Publish
</Button>
</div>
</form>
</div>
</div>
);
}
To upload the image after clicking on Publish button, you could simply move the uploading part inside addPost
, aka remove uploadImage
, and handle everything in one place, on submit
.
For that, set an imageInputRef
:
const imageInputRef = useRef<HTMLInputElement>(null);
Add it to your upload input (notice the onChange
is removed):
<input
id="images"
type="file"
className="relative block w-full appearance-none rounded-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
accept="image/*"
ref={imageInputRef}
/>
Change your Publish button as below (notice the onClick
is removed, and a type=submit
is added):
<Button type="sumbit" className="mt-5 bg-red-500">
Publish
</Button>
And finally, remove uploadImage
and change addPost
to:
async function addPost({
title,
description,
slug,
}: {
title: Posts["title"];
description: Posts["description"];
slug: Posts["slug"];
images: Posts['images'];
user_id: Posts["user_id"];
}) {
try {
if (!imageInputRef.current?.files || imageInputRef.current?.files.length === 0) {
// You could set some error message in a state here.
return;
}
setLoading(true);
const file = imageInputRef.current.files[0];
const data = new FormData();
data.append("file", file);
data.append("upload_preset", "c_tags");
const res = await fetch(`${process.env.NEXT_PUBLIC_CLOUDINARY_API}`, {
method: "POST",
body: data,
});
const returnedFile = await res.json();
const newImageData = { ...imageData, images: returnedFile.secure_url };
setImageData(newImageData);
const updates = {
title,
description,
slug,
images: `${newImageData.images}`,
created_at: new Date().toISOString(),
user_id: userDetails?.id,
};
let { error } = await supabase.from("posts").upsert(updates);
if (error) throw error;
alert("Published!");
} catch (error) {
alert("Error updating the data!");
console.log(error);
} finally {
setLoading(false);
}
}