casper's Profile Image

Full Stack Web/Mobile Developer

Jan, 24, 2025

Next Js Tutorial #15 - Ecommerce App - Product Reviews & Api Route

We created product reviews and api route.

Next Js Tutorial #15 - Ecommerce App - Product Reviews & Api Route Image

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

Comments (0)