casper's Profile Image

Full Stack Web/Mobile Developer

Jan, 19, 2025

Next Js Tutorial #10 - Ecommerce App - My Orders Page & Api Route

We added new data models in our schema and made functionality to create cart and products.

Next Js Tutorial #10 - Ecommerce App - My Orders Page & Api Route Image

You can also watch the YouTube video:

Next Js Tutorial #10 | Ecommerce App - My Orders Page & Api Route

Let's add a new data models to our schema file to make create cart and order functionality.

Edit schema.prisma in prisma folder:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

enum UserRole {
  USER
  ADMIN
}

enum OrderStatus {
  COMPLETED
  PENDING
  CANCELLED
}

model User {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  email     String   @unique
  password  String
  role      UserRole @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  orders    Order[]
}

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[]
}

model CartProduct {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  quantity  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  order   Order  @relation(fields: [orderId], references: [id])
  orderId String @db.ObjectId

  product   Product @relation(fields: [productId], references: [id])
  productId String  @db.ObjectId
}

model Order {
  id           String        @id @default(auto()) @map("_id") @db.ObjectId
  numId        Int           @unique
  status       OrderStatus   @default(PENDING)
  total        Float
  cartProducts CartProduct[]
  createdAt    DateTime      @default(now())
  updatedAt    DateTime      @updatedAt

  createdBy   User   @relation(fields: [createdById], references: [id])
  createdById String @db.ObjectId()
}

Push the new schema to our database:

npx prisma db push

Create route.js in app/api/orders 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 { cartProducts } = body;

  if (!cartProducts) {
    return NextResponse.json(
      { error: "An error occurred, please try again later!" },
      { 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 }
        );
      }

      let total = 0;

      await Promise.all(
        cartProducts.map(async (cartProduct) => {
          const product = await prisma.product.findUnique({
            where: { id: cartProduct.productId },
          });

          if (!product) {
            return NextResponse.json(
              { error: "Product not found!" },
              { status: 200 }
            );
          }

          total += product.price * cartProduct.quantity;
        })
      );

      const numId =
        Math.floor(Math.random() * (99999999999 - 10000000000 + 1)) +
        10000000000;

      const order = await prisma.order.create({
        data: {
          numId,
          total: total + 1,
          createdById: user.id,
          status: "COMPLETED",
        },
      });

      if (order) {
        await Promise.all(
          cartProducts.map(async (cartProduct) => {
            await prisma.cartProduct.create({
              data: {
                productId: cartProduct.productId,
                orderId: order.id,
                quantity: cartProduct.quantity,
              },
            });
          })
        );
      }

      return NextResponse.json(order, { status: 201 });
    }

    return NextResponse.json(
      { error: "Unauthorized request!" },
      { status: 200 }
    );
  } catch (error) {
    console.log(error.message);

    return NextResponse.json(
      { error: "Failed to create order" },
      { status: 500 }
    );
  } finally {
    await prisma.$disconnect();
  }
}

export async function GET() {
  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 orders = await prisma.order.findMany({
        orderBy: { createdAt: "desc" },
        where: { createdById: user.id },
        include: { cartProducts: { include: { product: true } } },
      });

      return NextResponse.json({ orders });
    } else {
      return NextResponse.json(
        { error: "Unauthorized request!" },
        { status: 200 }
      );
    }
  } catch (error) {
    console.log(error);

    return NextResponse.json(
      { error: "Failed to get orders" },
      { status: 500 }
    );
  } finally {
    await prisma.$disconnect();
  }
}

Create page.js in app/auth/my-orders folder:

import MyOrders from "@/components/Auth/MyOrders";

const AuthMyOrdersPage = () => {
  return <MyOrders />;
};

export default AuthMyOrdersPage;

Create MyOrders.jsx in components/Auth folder:

"use client";
import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";

const MyOrders = () => {
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(false);

  const getOrders = async () => {
    try {
      setLoading(true);
      const response = await fetch("/api/orders");

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      setOrders(data.orders);

      return data;
    } catch (error) {
      console.error("There was a problem with the fetch operation:", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    getOrders();
  }, []);

  return (
    <div className="mx-auto w-full max-w-screen-xl px-2.5 md:px-20 mt-16 space-y-8">
      {loading ? (
        <div className="flex items-center justify-center">
          <Loader2 className="w-8 h-8 animate-spin" />
        </div>
      ) : null}

      {orders
        ? orders.map((order) => (
            <div key={order.id}>
              <div className="rounded-lg border">
                {/* Card Header */}
                <div className="flex flex-col space-y-1.5 p-6">
                  {/* Card Title */}
                  <h1 className="text-2xl font-semibold leading-none tracking-tight">
                    Order #{order.numId}
                  </h1>

                  {/* Card Description */}
                  <div className="text-sm text-gray-900 dark:text-gray-50">
                    <p>{order.createdAt}</p>
                    <p>
                      Total:{" "}
                      <span className="font-semibold">${order.total} USD</span>
                    </p>
                  </div>
                </div>

                {/* Card Content */}
                <div className="p-6 pt-0">
                  <ul
                    className={
                      "divide-y divide-gray-200 border-b border-t border-gray-200"
                    }
                  >
                    {order.cartProducts
                      ? order.cartProducts.map((cartProduct) => {
                          return (
                            <li
                              key={cartProduct.id}
                              className="flex py-6 sm:py-10"
                            >
                              <div className="flex-shrink-0">
                                <div className="relative h-24 w-24">
                                  <Image
                                    fill
                                    src={cartProduct.product.image}
                                    alt="product image"
                                    className="h-full w-full rounded-md object-cover object-center sm:h-48 sm:w-48"
                                  />
                                </div>
                              </div>

                              <div className="ml-4 flex flex-1 flex-col justify-between sm:ml-6">
                                <div className="relative pr-9 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:pr-0">
                                  <div>
                                    <div className="flex justify-between">
                                      <h3 className="text-sm">
                                        <Link
                                          href={`/${cartProduct.product.id}`}
                                          className="font-medium text-gray-700 dark:text-gray-50 dark:hover:text-gray-200 hover:text-gray-800"
                                        >
                                          {cartProduct.product.title}{" "}
                                          {cartProduct.quantity > 1
                                            ? `(${cartProduct.quantity})`
                                            : null}
                                        </Link>
                                      </h3>
                                    </div>

                                    <p className="mt-1 text-sm font-medium text-gray-900 dark:text-gray-50">
                                      ${cartProduct.product.price} USD
                                    </p>
                                  </div>
                                </div>
                              </div>
                            </li>
                          );
                        })
                      : null}
                  </ul>
                </div>
              </div>
            </div>
          ))
        : null}
    </div>
  );
};

export default MyOrders;

Create layout.js in app/auth folder:

"use client";

import { useCurrentUser } from "@/contexts/CurrentUserContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function AuthLayout({ children }) {
  const { currentUser } = useCurrentUser();

  const router = useRouter();

  useEffect(() => {
    if (!currentUser) {
      router.push("/");
    }
  }, [currentUser]);

  return children;
}

Edit SidenavSheet.jsx in app/components folder:

        // Find the Home link
          <Link href="/">
            <li className="sidenav-li">
              <ArrowBigRight className="sidenav-li--icon" />

              <span className="ml-3">Home</span>
            </li>
          </Link>

        // Add My Orders link

          <Link href="/auth/my-orders">
            <li className="sidenav-li">
              <ArrowBigRight className="sidenav-li--icon" />

              <span className="ml-3">My Orders</span>
            </li>
          </Link>

Edit page.js in app/cart folder:

// Find this line

import { Minus, Plus, X } from "lucide-react";

// Edit with this line

import { Loader2, Minus, Plus, X } from "lucide-react";

// Add new imports

import { useRouter } from "next/navigation";
import { useState } from "react";

// Find this line
const { items, removeItem, increaseQuantity, decreaseQuantity, clearCart } =
  useCart();

// Add these lines above it
const [errorMsg, setErrorMsg] = useState("");
const [loading, setLoading] = useState(false);

const router = useRouter();

// Find and delete this line
const productIDs = items.map(({ product }) => product.id);

// Find this line
const cartTotal = items.reduce(
  (total, { product, quantity }) => total + product.price * quantity,
  0
);

// Add handleCheckout function below it

const handleCheckout = async () => {
  let cartProducts = [];

  items.map((item) => {
    cartProducts.push({
      productId: item.product.id,
      quantity: item.quantity,
    });
  });

  try {
    setLoading(true);

    const response = await fetch("/api/orders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        cartProducts,
      }),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();

    if (data.error) {
      setErrorMsg(data.error);

      setTimeout(() => {
        setErrorMsg("");
      }, 4000);
    } else {
      clearCart();

      router.push("/auth/my-orders");
    }

    return data;
  } catch (error) {
    console.error("There was a problem with the fetch operation:", error);
  } finally {
    setLoading(false);
  }
};

// Find H1 Tag
<h1 className="text-3xl font-bold tracking-tight  sm:text-4xl">
  Shopping Cart
</h1>;

// Add these lines below it

{
  errorMsg ? (
    <div
      className="p-4 mt-4 text-sm text-red-800 rounded-lg bg-red-50"
      role="alert"
    >
      <span className="font-medium">Error:</span>
      {errorMsg}
    </div>
  ) : undefined;
}

// And finally go to end of page and edit the button

<Button
  onClick={handleCheckout}
  disabled={items.length === 0 || loading}
  className="w-full"
  size="lg"
>
  {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Checkout"}
</Button>;

That's it for this tutorial, we added new data models in our schema and made functionality to create cart and products.

Next tutorial we'll create admin orders page and api route.

0
0