I am working on a Next.js application where users can add books using a form. Each book should have an uploaded cover image that gets stored in Supabase Storage, and its public URL should be saved in my book database table under the column bookImageUrl
.
What I Have So Far:
-
A React Hook Form (react-hook-form) handling the book details.
-
Supabase Storage setup to store book images
-
A separate component (UploadBookImage.tsx) to handle image uploads.
-
I need the uploaded image URL to be stored in the form state and submitted when saving
the book.
Expected Behavior:
-
The user selects an image file.
-
The image is uploaded to Supabase Storage.
-
The public URL of the uploaded image is retrieved and set in the form
state
-
The form is submitted, and the bookImageUrl is saved in the book
database.
Current Implementation
UploadBookImages.tsx Handle Images Upload
import { createClient } from "../../../../../utils/supabase/client";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export default function UploadBookImage({
onUpload,
}: {
size: number;
url: string | null;
onUpload: (url: string) => void;
}) {
const supabase = createClient();
const [uploading, setUploading] = useState(false);
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (
event
) => {
try {
setUploading(true);
if (!event.target.files || event.target.files.length === 0) {
throw new Error("You must select an image to upload.");
}
const file = event.target.files[0];
const fileExt = file.name.split(".").pop();
const filePath = `books/${Date.now()}.${fileExt}`;
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(filePath, file);
if (uploadError) {
throw uploadError;
}
onUpload(filePath);
} catch (error) {
alert(`Error uploading avatar! ${error}`);
} finally {
setUploading(false);
}
};
return (
<div>
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="picture">
{uploading ? "Uploading ..." : "Upload"}
</Label>
<Input
id="picture"
type="file"
accept="image/**"
onChange={uploadAvatar}
disabled={uploading}
name="bookImageUrl"
/>
</div>
</div>
);
}
Form
const BookForm: React.FC<BookFormProps> = ({ authors }) => {
const [state, action, pending] = useActionState(addBook, undefined);
const [bookImageUrl, setBookImageUrl] = useState<string | null>(null);
// React Hook Form with default values
const form = useForm<BookInferSchema>({
resolver: zodResolver(BookSchema),
defaultValues: {
//rest of the values
bookImageUrl: "",
},
});
//submitting the forms
async function onSubmit(data: BookInferSchema) {
try {
const formData = new FormData();
if (bookImageUrl) {
data.bookImageUrl = bookImageUrl; // Attach uploaded image URL
}
Object.entries(data).forEach(([key, value]) => {
formData.append(
key,
value instanceof Date ? value.toISOString() : value.toString()
);
});
//sending the formData to the action.ts for submitting the forms
const response = (await action(formData)) as {
error?: string;
message?: string;
} | void;
//Error or success messages for any submissions and any errors/success from the server
if (response?.error) {
toast({
title: "Error",
description: `An error occurred: ${response.error}`,
});
} else {
form.reset();
}
} catch {
toast({
title: "Error",
description: "An unexpected error occured.",
});
}
}
//Error or success messages for any submissions and any errors/success from the server
return (
<Form {...form}>
<form
className="space-y-8"
onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
form.handleSubmit(onSubmit)(e);
});
}}
>
<UploadBookImage
size={150}
url={bookImageUrl}
onUpload={(url) => setBookImageUrl(url)}
/>
//rest of the input fields
);
};
export default BookForm;
action.ts For saving the data in the database
"use server"
export async function addBook(state: BookFormState, formData: FormData) {
// Validate form fields
// Log all form data to debug
for (const pair of formData.entries()) {
console.log(`${pair[0]}: ${pair[1]}`);
}
const validatedFields = BookSchema.safeParse({
//rest of the values
bookImageUrl: formData.get("bookImageUrl"),
});
// Check if validation failed
if (!validatedFields.success) {
console.error("Validation Errors:", validatedFields.error.format()); // Log errors
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Prepare for insertion into the new database
const {..rest of the values, bookImageUrl} = validatedFields.data
// Insert the new author into the database
const supabase = createClient();
const {data, error} = await (await supabase).from('books').insert({ ...rest of the values, bookImageUrl});
if(data){
console.log(data,"data in the addBook function")
}
if (error) {
return {
error: true,
message: error.message,
};
}
return {
error: false,
message: 'Book updated successfully',
};
}
Data definition from Supabase and RLS policy
create table
public.books (
//rest of the columns
"bookImageUrl" text null,
constraint books_pkey primary key (isbn),
constraint books_author_id_fkey foreign key (author_id) references authors (id) on delete cascade
) tablespace pg_default;
RLS policy for now:
alter policy "Enable insert for authenticated users only"
on "public"."books"
to authenticated
with check (
true
);
Storage bucket:
My schema
import { z } from "zod";
export const BookSchema = z.object({
//rest of the values
bookImageUrl :z.string().optional()
});
// TypeScript Type for Book
export type BookInferSchema = z.infer<typeof BookSchema>;
//Form state for adding and editing books
export type BookFormState =
| {
errors?: {
//rest of the values
bookImageUrl?: string[];
};
message?: string;
}
| undefined;
Issues I’m facing:
- Unable to upload in the storage bucket
book-pics
. Hence, I am unable to save the bookImageURL
when I submit the form.