
Next Js Tutorial #15 - Ecommerce App - Product Reviews & Api Route
We created product reviews and api route.

You can also watch the YouTube video:
Next Js Tutorial #15 | Ecommerce App - Product Reviews & Api Route
Installing Required ShadCN UI Components
npx shadcn@latest add textarea
Edit schema.prisma
in prisma
folder:
// Updated User model
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
password String
fullName String?
address String?
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
bookmarks Bookmark[]
likes Like[]
reviews Review[]
}
// Updated Product Model
model Product {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
description String
price Int
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cartProducts CartProduct[]
bookmarks Bookmark[]
likes Like[]
reviews Review[]
}
// New Review Model
model Review {
id String @id @default(auto()) @map("_id") @db.ObjectId
rate Int
comment String
product Product @relation(fields: [productId], references: [id])
productId String @db.ObjectId
user User @relation(fields: [userId], references: [id])
userId String @db.ObjectId
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
Push the new schema to our database:
npx prisma db push
Edit page.js
in app/[id]
folder:
// Add new import
import ProductReviews from "@/components/ProductReviews";
// Add ProductReviews component top of the last div
<ProductReviews product={product} />
</div>
);
};
export default Product;
Edit route.js
in app/api/products/[id]
folder:
// Find this line
const { id } = params;
// Change with this line
const { id } = await params;
// Find this line
const product = await prisma.product.findUnique({
where: { id },
});
// Change with this line
const product = await prisma.product.findUnique({
where: { id },
include: { reviews: { include: { user: true } } },
});
Create route.js
in app/api/auth/reviews
folder:
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { cookies } from "next/headers";
import jwt from "jsonwebtoken";
const prisma = new PrismaClient();
export async function POST(request) {
const body = await request.json();
const { productId, rate, comment } = body;
if (!productId || !rate || !comment) {
return NextResponse.json(
{ error: "You must fill all the required fields!" },
{ status: 200 }
);
}
try {
const tokenCookie = await cookies();
const getToken = tokenCookie.get("token");
if (getToken) {
const token = jwt.verify(getToken.value, "appSecret");
const userId = token.id;
if (!userId) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
const review = await prisma.review.create({
data: {
productId,
rate,
comment,
userId: user.id,
},
});
return NextResponse.json(review, { status: 201 });
}
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
} catch (error) {
console.log(error.message);
return NextResponse.json(
{ error: "Failed to create review" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}
Create ProductReviews.jsx
in components
folder:
"use client";
import { Loader2, Verified } from "lucide-react";
import { FaStar } from "react-icons/fa";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
import { useState } from "react";
import { useCurrentUser } from "@/contexts/CurrentUserContext";
const ProductReviews = ({ product }) => {
const [comment, setComment] = useState();
const [rate, setRate] = useState(0);
const [loading, setLoading] = useState(false);
const { currentUser } = useCurrentUser();
const handleReview = async () => {
if (currentUser) {
try {
setLoading(true);
const response = await fetch("/api/auth/reviews", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
productId: product.id,
rate,
comment,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
} finally {
setLoading(false);
window.location.reload();
}
} else {
alert("You need to sign in to review a product!");
}
};
return (
<section className="py-8 antialiased md:py-16">
<div className="mx-auto max-w-screen-xl px-4 2xl:px-0">
<div className="flex items-center gap-2">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
Reviews
</h2>
</div>
<div className="space-y-4 mt-4 md:max-w-[60%]">
<div className="flex items-center gap-0.5">
<FaStar
onClick={() => setRate(1)}
className={
rate >= 1
? "fill-yellow-500 cursor-pointer"
: "cursor-pointer hover:fill-yellow-500"
}
/>
<FaStar
onClick={() => setRate(2)}
className={
rate >= 2
? "fill-yellow-500 cursor-pointer"
: "cursor-pointer hover:fill-yellow-500"
}
/>
<FaStar
onClick={() => setRate(3)}
className={
rate >= 3
? "fill-yellow-500 cursor-pointer"
: "cursor-pointer hover:fill-yellow-500"
}
/>
<FaStar
onClick={() => setRate(4)}
className={
rate >= 4
? "fill-yellow-500 cursor-pointer"
: "cursor-pointer hover:fill-yellow-500"
}
/>
<FaStar
onClick={() => setRate(5)}
className={
rate === 5
? "fill-yellow-500 cursor-pointer"
: "cursor-pointer hover:fill-yellow-500"
}
/>
</div>
<Textarea
placeholder="Make a review"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<Button disabled={loading} className="btn" onClick={handleReview}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Send"}
</Button>
</div>
<div className="mt-6 divide-y divide-gray-200 dark:divide-gray-700">
{product?.reviews?.map((review) => {
const formattedDate = new Intl.DateTimeFormat("en-US", {
month: "long", // Full month name
day: "2-digit", // Two-digit day
year: "numeric", // Full year
}).format(new Date(review.createdAt));
return (
<div
key={review.id}
className="gap-3 p-4 pl-0 sm:flex sm:items-start"
>
<div className="shrink-0 space-y-2 sm:w-48 md:w-72">
<div className="flex items-center gap-0.5">
<FaStar
className={
review.rate >= 1 ? "fill-yellow-500" : undefined
}
/>
<FaStar
className={
review.rate >= 2 ? "fill-yellow-500" : undefined
}
/>
<FaStar
className={
review.rate >= 3 ? "fill-yellow-500" : undefined
}
/>
<FaStar
className={
review.rate >= 4 ? "fill-yellow-500" : undefined
}
/>
<FaStar
className={
review.rate === 5 ? "fill-yellow-500" : undefined
}
/>
</div>
<div className="space-y-0.5">
<p className="text-base font-semibold text-gray-900 dark:text-white">
{review.user.fullName
? review.user.fullName
: review.user.email}
</p>
<p className="text-sm font-normal text-gray-500 dark:text-gray-400">
{formattedDate}
</p>
</div>
<div className="inline-flex items-center gap-1">
<Verified className="h-5 w-5 text-blue-700 dark:text-blue-500" />
<p className="text-sm font-medium text-gray-900 dark:text-white">
Verified purchase
</p>
</div>
</div>
<div className="mt-4 min-w-0 flex-1 space-y-4 sm:mt-0">
<p className="text-base font-normal text-gray-500 dark:text-gray-400">
{review.comment}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
export default ProductReviews;
That's it for this tutorial, we created product reviews and api route.
0
0