My name is Tom and I’m a current Software Developer graduate. Over the course of the last few months, I’ve spend some of my free time building an online bookstore.
For this store, I’ve followed a tutorial of a Youtuber by the name of ‘Web Dev Simplified’. While he builds his store with Next.js, I’ve tried to convert his code to normal React and added some Material UI elements to it.
While my site looks decent in itself, I am currently stuck with a major problem. The form, which I use to add new products to my local database, does not seem to work. I’ve tried everything I could think of to fix the problem, but to no avail. Perhaps someone here could help me with it?
This is the code for my Form:
import {
Box,
Button,
Container,
FormControl,
FormHelperText,
FormLabel,
Input,
Stack,
} from "@mui/material";
import AdminLayout from "../../Layout";
import { useState } from "react";
import { Product } from "@prisma/client";
import { formatCurrency } from "../../../../lib/formatters";
import Header from "../../../components/Header";
import EuroIcon from "@mui/icons-material/Euro";
import { addProduct, updateProduct } from "../../../actions/products";
import { useFormState, useFormStatus } from "react-dom";
const ProductForm = () => {
return (
<AdminLayout>
<Header>Add New Product</Header>
<Form />
</AdminLayout>
);
};
const Form = ({ product }: { product?: Product | null }) => {
const [priceInCents, setPriceInCents] = useState<number | undefined>(
product?.priceInCents
);
const [type, setType] = useState<string | undefined>(product?.type);
const [error, action] = useFormState(
product == null ? addProduct : updateProduct.bind(null, product.id),
{}
);
return (
<form action={action}>
<Container
sx={{
display: "grid",
justifyContent: "center",
padding: 3,
}}
>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="title">Title</FormLabel>
<Input
type="text"
id="title"
name="title"
margin="dense"
defaultValue={product?.title || ""}
required
/>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="author">Author</FormLabel>
<Input
type="text"
id="author"
name="author"
margin="dense"
defaultValue={product?.author || ""}
required
/>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="artist">Artist</FormLabel>
<Input
type="text"
id="artist"
name="artist"
margin="dense"
defaultValue={product?.artist || ""}
required
/>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="priceInCents">Price In Cents</FormLabel>
<Input
type="number"
id="priceInCents"
name="priceInCents"
margin="dense"
endAdornment={
<EuroIcon sx={{ fontSize: "large", color: "gray" }} />
}
value={priceInCents}
onChange={(e) =>
setPriceInCents(Number(e.target.value) || undefined)
}
defaultValue={product?.priceInCents || ""}
required
/>
<Box sx={{ marginBottom: 2, fontSize: 14, color: "grey" }}>
{formatCurrency((priceInCents || 0) / 100)}
</Box>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="pages">Number Of Pages</FormLabel>
<Input
type="number"
id="pages"
name="pages"
margin="dense"
defaultValue={product?.pages || ""}
required
/>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="description">Description</FormLabel>
<Input
type="text"
id="description"
name="description"
margin="dense"
defaultValue={product?.description || ""}
multiline
required
/>
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="file">Product File</FormLabel>
<Input type="file" id="file" name="file" margin="dense" required />
</FormControl>
<FormControl variant="outlined" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="cover">Image File</FormLabel>
<Input type="file" id="cover" name="cover" margin="dense" required />
</FormControl>
<FormControl>
<Stack direction="column" sx={{ paddingBottom: 2 }}>
<FormLabel htmlFor="type">Type Of Product</FormLabel>
<Input
type="text"
id="type"
name="type"
defaultValue={product?.type || ""}
onChange={(e) => setType(e.target.value)}
error={
type != "Novel" && type != "Comic" && type != "" && type != null
}
required
/>
<FormHelperText id="type-helper-text">
Like 'Novel' or 'Comic'
</FormHelperText>
</Stack>
</FormControl>
<SubmitButton />
</Container>
</form>
);
};
const SubmitButton = () => {
const { pending } = useFormStatus();
return (
<Button type="submit" variant="contained" disabled={pending}>
{pending ? "Saving..." : "Save"}
</Button>
);
};
export default ProductForm;
And this is the code for my addProduct action:
import { z } from "zod";
import fs from "fs/promises";
import db from "../../db/db";
const fileSchema = z.instanceof(File, { message: "Required" });
const imageSchema = fileSchema.refine(
(file) => file.size === 0 || file.type.startsWith("image/")
);
const addSchema = z.object({
title: z.string().min(1),
author: z.string().min(1),
artist: z.string().min(1),
description: z.string().min(1),
type: z.string().min(1),
priceInCents: z.coerce.number().int().min(1),
pages: z.coerce.number().int().min(1),
file: fileSchema.refine((file) => file.size > 0, "Required"),
image: imageSchema.refine((file) => file.size > 0, "Required"),
});
export async function addProduct(prevState: unknown, formData: FormData) {
const result = addSchema.safeParse(Object.fromEntries(formData.entries()));
if (result.success === false) {
return result.error.formErrors.fieldErrors;
}
const data = result.data;
fs.mkdir("products", { recursive: true });
const filePath = `products/${crypto.randomUUID()}-${data.file.name}`;
fs.writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));
fs.mkdir("public/products", { recursive: true });
const imagePath = `products/${crypto.randomUUID()}-${data.image.name}`;
fs.writeFile(
`public${imagePath}`,
Buffer.from(await data.image.arrayBuffer())
);
db.product.create({
data: {
isAvailable: false,
title: data.title,
author: data.author,
artist: data.artist,
pages: data.pages,
description: data.description,
priceInCents: data.priceInCents,
type: data.type,
filePath,
imagePath,
},
});
}
Tom Kooij is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.