DEV Community

Cover image for Fullstack ecommerce site using Nextjs, Supabase, prisma and chatwoot.šŸ”„šŸ”„
Pramit Marattha
Pramit Marattha

Posted on • Edited on

Fullstack ecommerce site using Nextjs, Supabase, prisma and chatwoot.šŸ”„šŸ”„

In this article tutorial, we will be building SuperbaseEcommerce full-stack application. This application is simply an online ecommerce shopping site where users can browse all of the products, upload their own products, and even purchase the products(This functionality will be added in the next series of articles). It is similar to an Amazon app, but it is simpler because we will not implement any actual payment or shipping procedures. Here's a live demonstration of the final version of the app. This is how your app should look after you finish this tutorial. Feel free to experiment with it to get a sense of all the features we will be implementing.

Live Demo => https://supabase-ecommerce.vercel.app

Supabase Ecommerce logo

Demo

Demo

So, in this tutorial, we'll learn how to build this full-stack app with Next.js, the react framework, NextAuth.js, for implementing passwordless and OAuth authentication, Supabase, for persisting app data into a PostgreSQL database and stashing media files and information, and Prisma, for making it simple to read and write data from and to the database from our app.

This article tutorial covers many topics and technical concepts necessary to build a modern full-stack app, even if this app is a simplified version of a more advanced ecommerce site like Amazon. You should be able to use all of the technologies covered in this tutorial, including react, nextjs, prisma, supabase, and others, but most importantly, you should be able to build any full-stack app using those technologies. You'll go at your own speed and intensity, with us guiding you along the way. After completing this guide, the goal of this article is to provide you with the tools and techniques you'll need to build a similar app on your own.To put it another way, this tutorial will not only teach you how to use those technologies in great detail, but it will also provide you with the proper mixture of principles and application to help you grasp all of the key concepts so that you can proudly build your own apps from scratch later part on this article.

Let's start with the react portion and build our application. The first step is to install Node.js if it isn't already on your computer. So, go to the official Node.js website and download the most recent version. Node js is required to use the node package manager, abbreviated as npm. Now launch your preferred code editor and navigate to the folder. For this article tutorial, we'll be using the VScode code editor.

Setting up SupabaseEcommerce project.

There is a Github repository dedicated to this project, which consists of three branches. Clone the SupabaseEcommerce-starter branch to get started.

Github

The Main branch contains the entire final source code of the application, so clone the SupabaseEcommerce-starter branch if you want to follow along with this tutorial.

git clone --branch SupabaseEcommerce-starter https://github.com/pramit-marattha/SupabaseEcommerce.git
Enter fullscreen mode Exit fullscreen mode

After that, head over to the cloned directory and install the dependencies before starting the Next.js development server:

cd SupabaseEcommerce
yarn add all
yarn dev
Enter fullscreen mode Exit fullscreen mode

You can now check if everything is working properly by going to http://localhost:3000 and editing pages/index.js, then viewing the updated result in your browser.For more information on how to use create-next-app, you can review the create-next-app documentation.

Documentation

It usually only takes a few minutes to get everything set up. So, for this project we will be using yarn to add packages to a project, which will install and configure everything for us so that we can get started right away with an excellent starter template. It's time to start our development server, so head over to that SupabaseEcommerce folder and type yarn add all and then yarn dev and the browser will instantly open our starter template Next.js appplication.

Demo

Your applicationā€™s folder structure should look something like this.

Folder structure

So you might be curious about the source of the content. Remember that all of our source code is housed in theĀ pagesĀ folder, and react/next will inject it into the rootĀ divĀ element. Letā€™s take a look at ourĀ pagesĀ folder, which contains some javascript files and one API folder.

Pages

Before we dive any further lets actually create a landing page for our site.

so before we even begin first you need to install framer-motion library.

Framer Motion

Let's dive in and create a beautiful looking UI for our E-commerce application before we start on the backend integration part. Let's start by making a landing page for the app, and then move on to making a product page for it. So, inside the components folder, create a Layout component and add the following code to it. This component is simply a basic layout for our application that includes a navigation bar & menus as well as the functionality to display our application's registration/login modal.

// components/Layout.js
import { Fragment, useState } from "react";
import { useRouter } from "next/router";
import Head from "next/head";
import Link from "next/link";
import Image from "next/image";
import PropTypes from "prop-types";
import AuthModal from "./AuthModal";
import { Menu, Transition } from "@headlessui/react";
import {
  HeartIcon,
  HomeIcon,
  LogoutIcon,
  PlusIcon,
  UserIcon,
  ShoppingCartIcon,
} from "@heroicons/react/outline";
import { ChevronDownIcon } from "@heroicons/react/solid";

const menuItems = [
  {
    label: "List a new home",
    icon: PlusIcon,
    href: "/list",
  },
  {
    label: "My homes",
    icon: HomeIcon,
    href: "/homes",
  },
  {
    label: "Favorites",
    icon: HeartIcon,
    href: "/favorites",
  },
  {
    label: "Logout",
    icon: LogoutIcon,
    onClick: () => null,
  },
];

const Layout = ({ children = null }) => {
  const router = useRouter();

  const [showModal, setShowModal] = useState(false);

  const user = null;
  const isLoadingUser = false;

  const openModal = () => setShowModal(true);
  const closeModal = () => setShowModal(false);

  return (
    <>
      <Head>
        <title>SupaaShop | A new way to shop!</title>
        <meta name="title" content="SupaaShopp" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="min-h-screen flex flex-col font-['Poppins'] bg-[linear-gradient(90deg, #161122 21px, transparent 1%) center, linear-gradient(#161122 21px, transparent 1%) center, #a799cc]">
        <header className="h-28 w-full shadow-lg">
          <div className="h-full container mx-auto">
            <div className="h-full px-5 flex justify-between items-center space-x-5">
              <Link href="/">
                <a className="flex items-center space-x-1">
                  <img
                    className="shrink-0 w-24 h-24 text-primary"
                    src="https://user-images.githubusercontent.com/37651620/158058874-6a86646c-c60e-4c39-bc6a-d81974afe635.png"
                    alt="Logo"
                  />
                  <span className="text-2xl font-semibold tracking-wide text-white">
                    <span className="text-3xl text-success">S</span>upabase
                    <span className="text-3xl text-success">E</span>commerce
                  </span>
                </a>
              </Link>
              <div className="flex items-center space-x-4">
                <Link href="/create">
                  <a className="ml-4 px-4 py-5 rounded-md bg-info text-primary hover:bg-primary hover:text-info focus:outline-none focus:ring-4 focus:ring-primaryfocus:ring-opacity-50  font-semibold transition">
                    Register shop !
                  </a>
                </Link>
                {isLoadingUser ? (
                  <div className="h-8 w-[75px] bg-gray-200 animate-pulse rounded-md" />
                ) : user ? (
                  <Menu as="div" className="relative z-50">
                    <Menu.Button className="flex items-center space-x-px group">
                      <div className="shrink-0 flex items-center justify-center rounded-full overflow-hidden relative bg-gray-200 w-9 h-9">
                        {user?.image ? (
                          <Image
                            src={user?.image}
                            alt={user?.name || "Avatar"}
                            layout="fill"
                          />
                        ) : (
                          <UserIcon className="text-gray-400 w-6 h-6" />
                        )}
                      </div>
                      <ChevronDownIcon className="w-5 h-5 shrink-0 text-gray-500 group-hover:text-current" />
                    </Menu.Button>
                    <Transition
                      as={Fragment}
                      enter="transition ease-out duration-100"
                      enterFrom="opacity-0 scale-95"
                      enterTo="opacity-100 scale-100"
                      leave="transition ease-in duration-75"
                      leaveFrom="opacity-100 scale-100"
                      leaveTo="opacity-0 scale-95"
                    >
                      <Menu.Items className="absolute right-0 w-72 overflow-hidden mt-1 divide-y divide-gray-100 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                        <div className="flex items-center space-x-2 py-4 px-4 mb-2">
                          <div className="shrink-0 flex items-center justify-center rounded-full overflow-hidden relative bg-gray-200 w-9 h-9">
                            {user?.image ? (
                              <Image
                                src={user?.image}
                                alt={user?.name || "Avatar"}
                                layout="fill"
                              />
                            ) : (
                              <UserIcon className="text-gray-400 w-6 h-6" />
                            )}
                          </div>
                          <div className="flex flex-col truncate">
                            <span>{user?.name}</span>
                            <span className="text-sm text-gray-500">
                              {user?.email}
                            </span>
                          </div>
                        </div>
                        <div className="py-2">
                          {menuItems.map(
                            ({ label, href, onClick, icon: Icon }) => (
                              <div
                                key={label}
                                className="px-2 last:border-t last:pt-2 last:mt-2"
                              >
                                <Menu.Item>
                                  {href ? (
                                    <Link href={href}>
                                      <a className="flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100">
                                        <Icon className="w-5 h-5 shrink-0 text-gray-500" />
                                        <span>{label}</span>
                                      </a>
                                    </Link>
                                  ) : (
                                    <button
                                      className="w-full flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100"
                                      onClick={onClick}
                                    >
                                      <Icon className="w-5 h-5 shrink-0 text-gray-500" />
                                      <span>{label}</span>
                                    </button>
                                  )}
                                </Menu.Item>
                              </div>
                            )
                          )}
                        </div>
                      </Menu.Items>
                    </Transition>
                  </Menu>
                ) : (
                  <button
                    type="button"
                    onClick={openModal}
                    className="ml-4 px-4 py-5 rounded-md bg-info hover:bg-primary focus:outline-none focus:ring-4 focus:ring-primary focus:ring-opacity-50 text-primary hover:text-info font-extrabold transition"
                  >
                    Login
                  </button>
                )}
              </div>
            </div>
          </div>
        </header>

        <main className="flex-grow container mx-auto">
          <div className="px-4 py-12">
            {typeof children === "function" ? children(openModal) : children}
          </div>
        </main>

        <AuthModal show={showModal} onClose={closeModal} />
      </div>
    </>
  );
};

Layout.propTypes = {
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
};

export default Layout;
Enter fullscreen mode Exit fullscreen mode

Let's create a 'Hero' section of our landing page after you've successfully created a layout for the application. To do so, simply paste the following code into that section. So, in this section, we'll add an image on the right, a large text heading, and two buttons on the left.Note that we are styling our project with the absolute power of tailwind css and framer-motion to add some beautiful transition animation to the image .Since we've already created buttons on our starter template, you won't have to worry about creating them from scratch; instead, you can simply import them from the components and use them.

// components/Hero.js
import React from "react";
import PrimaryButton from "@/components/PrimaryButton";
import SecondaryButton from "@/components/SecondaryButton";
import { motion } from "framer-motion";

const Hero = () => {
  return (
    <div className="max-w-6xl mx-auto py-12 flex flex-col md:flex-row space-y-8 md:space-y-0">
      <div className="w-full md:w-1/2 flex flex-col justify-center items-center">
        <div className="max-w-xs lg:max-w-md space-y-10 w-5/6 mx-auto md:w-full text-center md:text-left">
          <h1 className="font-primary font-extrabold text-white text-3xl sm:text-4xl md:text-5xl md:leading-tight">
            Shop <span className="text-success">whenever</span> and{" "}
            <span className="text-success">however</span> you want from,{" "}
            <span className="text-success">wherever</span> you are..{" "}
          </h1>
          <p className="font-secondary text-gray-500 text-base md:text-lg lg:text-xl">
            SuperbaseEcommerce improves and streamlines your shopping
            experience..
          </p>
          <div className="flex space-x-4">
            <PrimaryButton text="Register" link="/" />
            <SecondaryButton text="Let's Shop!" link="/products" />
          </div>
        </div>
      </div>
      <motion.div
        className="w-full md:w-1/2 transform scale-x-125 lg:scale-x-100"
        initial={{ opacity: 0, translateY: 60 }}
        animate={{ opacity: 1, translateY: 0 }}
        transition={{ duration: 0.8, translateY: 0 }}
      >
        <img
          alt="hero-img"
          src="./assets/shop.svg"
          className="mx-auto object-cover shadow rounded-tr-extraLarge rounded-bl-extraLarge w-full h-96 sm:h-112 md:h-120"
        />
      </motion.div>
    </div>
  );
};

export default Hero;
Enter fullscreen mode Exit fullscreen mode

Now, before re-running the server, import this Hero component into the index.js file and wrap it in the Layout component to see the changes you've made.

// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";

export default function Home() {
  return (
    <Layout>
      <Hero />
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is how your landing page should appear.

Demo

After you've finished with the Hero section, go ahead and create a ShopCards component, where we'll simply list the demo features that this application offers and add some images, so your final code for the ShopCards component should look like this.

// components/ShopCards.js
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";

const ShopCards = () => {
  const [tab, setTab] = useState(1);

  const tabs = useRef(null);

  const heightFix = () => {
    if (tabs.current.children[tab]) {
      tabs.current.style.height =
        tabs.current.children[tab - 1].offsetHeight + "px";
    }
  };

  useEffect(() => {
    heightFix();
  }, [tab]);
  return (
    <section className="relative">
      <div
        className="absolute inset-0 pointer-events-none pb-26"
        aria-hidden="true"
      ></div>

      <div className="relative max-w-6xl mx-auto px-4 sm:px-6">
        <div className="pt-12 md:pt-20">
          <div className="max-w-3xl mx-auto text-center pb-12 md:pb-16">
            <h1 className="text-3xl mb-4">Features</h1>
            <p className="text-xl text-gray-500">
              List of features that SuperbaseEcommerce provides.
            </p>
          </div>
          <div className="relative max-w-6xl mx-auto px-4 sm:px-6">
            <div className="pt-12 md:pt-20">
              <div className="max-w-3xl mx-auto text-center pb-6 md:pb-16">
                <div className="" data-aos="zoom-y-out" ref={tabs}>
                  <motion.div
                    className="relative w-full h-full"
                    initial={{ opacity: 0, translateY: 60 }}
                    animate={{ opacity: 1, translateY: 0 }}
                    transition={{ duration: 0.8, translateY: 0 }}
                  >
                    <img
                      alt="hero-img"
                      src="./assets/webShop.svg"
                      className="mx-auto object-cover shadow rounded-tr-extraLarge rounded-bl-extraLarge w-full h-96 sm:h-112 md:h-120"
                    />
                  </motion.div>
                </div>
              </div>
            </div>
          </div>

          <div className="max-w-6xl mx-auto py-12 flex flex-col md:flex-row space-y-8 md:space-y-0">
            <div
              className="max-w-xl md:max-w-none md:w-full mx-auto md:col-span-7 lg:col-span-6 md:mt-6 pr-12"
              data-aos="fade-right"
            >
              <div className="md:pr-4 lg:pr-12 xl:pr-16 mb-8">
                <h3 className="h3 mb-3">All of our awesome features</h3>
                <p className="text-xl text-black"></p>
              </div>
              <div className="mb-8 md:mb-0">
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 1
                      ? "bg-white shadow-md border-success hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(1);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Register/Login Feature
                    </div>
                    <div className="text-gray-600">
                      User can login and save their products for later purchase.
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 2
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(2);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Add to cart
                    </div>
                    <div className="text-gray-600">
                      User can add the products/items to their cart
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 3
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(3);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Security
                    </div>
                    <div className="text-gray-600">
                      Hassle free secure login and registration process.
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 4
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(4);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Personalized shops
                    </div>
                    <div className="text-gray-600">
                      User can create/register their very own shop and add their
                      own products.
                    </div>
                  </div>
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default ShopCards;
Enter fullscreen mode Exit fullscreen mode

Again, before re-running the server, import this ShopCards component into the index.js file and wrap it in the Layout component & below the Hero component to see the changes you've made.

// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";
import ShopCards from "@/components/ShopCards";

export default function Home() {
  return (
    <Layout>
      <Hero />
      <ShopCards />
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

For the time being, this is how your landing page should appear.

Demo

Finally, let's add a Footer section, so make a Footer component and paste the code below into it.

// components/Footer.js
import Link from "next/link";

const Footer = () => {
  return (
    <footer>
      <div className="max-w-6xl mx-auto px-4 sm:px-6 pt-10">
        <div className="sm:col-span-6 md:col-span-3 lg:col-span-3">
          <section>
            <div className="max-w-6xl mx-auto px-4 sm:px-6">
              <div className="pb-12 md:pb-20">
                <div
                  className="relative bg-success rounded py-10 px-8 md:py-16 md:px-12 shadow-2xl overflow-hidden"
                  data-aos="zoom-y-out"
                >
                  <div
                    className="absolute right-0 bottom-0 pointer-events-none hidden lg:block"
                    aria-hidden="true"
                  ></div>

                  <div className="relative flex flex-col lg:flex-row justify-between items-center">
                    <div className="text-center lg:text-left lg:max-w-xl">
                      <h6 className="text-gray-600 text-3xl font-medium mb-2">
                        Sign-up for the early access!{" "}
                      </h6>
                      <p className="text-gray-100 text-lg mb-6">
                        SuperbaseEcommerce improves and streamlines your
                        shopping experience.. !
                      </p>
                      <form className="w-full lg:w-auto">
                        <div className="flex flex-col sm:flex-row justify-center max-w-xs mx-auto sm:max-w-xl lg:mx-0">
                          <input
                            type="email"
                            className="w-full appearance-none bg-purple-100 border border-gray-700 focus:border-gray-600 rounded-sm px-4 py-3 mb-2 sm:mb-0 sm:mr-2 text-black placeholder-gray-500"
                            placeholder="Enter your emailā€¦"
                            aria-label="Enter your emailā€¦"
                          />
                          <a
                            className="btn text-white bg-info hover:bg-success shadow"
                            href="#"
                          >
                            Sign-Up!
                          </a>
                        </div>
                      </form>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </section>
        </div>
        <div className="md:flex md:items-center md:justify-between py-4 md:py-8 border-t-2 border-solid">
          <ul className="flex mb-4 md:order-1 md:ml-4 md:mb-0">
            <li>
              <Link
                href="#"
                className="flex justify-center items-center text-blue-400 hover:text-gray-900 bg-blue-100 hover:bg-white-100 rounded-full shadow transition duration-150 ease-in-out"
                aria-label="Twitter"
              >
                <svg
                  className="w-8 h-8 fill-current "
                  viewBox="0 0 32 32"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path d="M24 11.5c-.6.3-1.2.4-1.9.5.7-.4 1.2-1 1.4-1.8-.6.4-1.3.6-2.1.8-.6-.6-1.5-1-2.4-1-1.7 0-3.2 1.5-3.2 3.3 0 .3 0 .5.1.7-2.7-.1-5.2-1.4-6.8-3.4-.3.5-.4 1-.4 1.7 0 1.1.6 2.1 1.5 2.7-.5 0-1-.2-1.5-.4 0 1.6 1.1 2.9 2.6 3.2-.3.1-.6.1-.9.1-.2 0-.4 0-.6-.1.4 1.3 1.6 2.3 3.1 2.3-1.1.9-2.5 1.4-4.1 1.4H8c1.5.9 3.2 1.5 5 1.5 6 0 9.3-5 9.3-9.3v-.4c.7-.5 1.3-1.1 1.7-1.8z" />
                </svg>
              </Link>
            </li>
            <li className="ml-4">
              <Link
                href="#"
                className="flex justify-center items-center text-white hover:text-gray-900 bg-black hover:bg-white-100 rounded-full shadow transition duration-150 ease-in-out"
                aria-label="Github"
              >
                <svg
                  className="w-8 h-8 fill-current"
                  viewBox="0 0 32 32"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path d="M16 8.2c-4.4 0-8 3.6-8 8 0 3.5 2.3 6.5 5.5 7.6.4.1.5-.2.5-.4V22c-2.2.5-2.7-1-2.7-1-.4-.9-.9-1.2-.9-1.2-.7-.5.1-.5.1-.5.8.1 1.2.8 1.2.8.7 1.3 1.9.9 2.3.7.1-.5.3-.9.5-1.1-1.8-.2-3.6-.9-3.6-4 0-.9.3-1.6.8-2.1-.1-.2-.4-1 .1-2.1 0 0 .7-.2 2.2.8.6-.2 1.3-.3 2-.3s1.4.1 2 .3c1.5-1 2.2-.8 2.2-.8.4 1.1.2 1.9.1 2.1.5.6.8 1.3.8 2.1 0 3.1-1.9 3.7-3.7 3.9.3.4.6.9.6 1.6v2.2c0 .2.1.5.6.4 3.2-1.1 5.5-4.1 5.5-7.6-.1-4.4-3.7-8-8.1-8z" />
                </svg>
              </Link>
            </li>
          </ul>

          <div className="flex-shrink-0 mr-2">
            <Link href="/" className="block" aria-label="SuperbaseEcommerce">
              <img
                className="object-cover h-20 w-full"
                src="https://user-images.githubusercontent.com/37651620/159121520-fe42bbf1-a2af-4baf-bdd8-7efad8523202.png"
                alt="SupabaseEcommerce"
              />
            </Link>
          </div>
        </div>
      </div>
    </footer>
  );
};

export default Footer;
Enter fullscreen mode Exit fullscreen mode

Note: Again, before re-running the server, import this Footer component into the index.js file and wrap it in the Layout component & below the ShopCards component to see the changes you've made.

// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";
import ShopCards from "@/components/ShopCards";
import Footer from "@/components/Footer";

export default function Home() {
  return (
    <Layout>
      <Hero />
      <ShopCards />
      <Footer />
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

So, if you re-run the server, this is what your application should look like.

Demo Footer

The structure of your component folders should resemble something like this.

Demo

Congratulationss!! Now that you've successfully created a landing page for the application, let's move on to the core of the matter: creating the product section of the application.

So, Now letā€™s look at theĀ _app.jsĀ file.

// _app.js
import "../styles/globals.css";
import { Toaster } from "react-hot-toast";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Toaster />
    </>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

The App component is used by Next.js to create pages. You can control the page initialization by simply overriding it. It allows you to do amazing things like: Persisting layout across page changes, Keeping state while navigating pages, Custom error handling using componentDidCatch,Inject additional data into pages and Add global styles/CSS are just a few of the great things you can accomplish with it.

In the above \_app.js code the Component parameter represents the active page, when you switch routes, Component will change to the new page. As a result, the page will receive any props you pass to Component. Meanwhile pageProps is an empty object that contains the initial props that were preloaded for your page by one of the data fetching methods.

Now, inside the pages folder, create a new page called products.js and import the Layout and Grid components, then import the data.json file as products and make the following changes to it.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "data.json";

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Database Configurations

Before jumping directly on our application, we'll be utilizing the power of Supabase to create a PostgreSQL database, the Prisma schema to define the app data model, and Next.js to connect those two together. So, let's get started building our database.

Supabase Configurartion

Creating a PostgreSQL database in Supabase is as simple as starting a new project. Head over to supabase.com and Sign-in to your account.

Supabase

After you've successfully signed in, you should see something similar to this.

New project

Now, select New project button. Fill in your project's required details and again click Create Project button and wait for the new database to load.

Create Project

Creating project

After the supabase configured the project, your dashboard should look something similar to this.

SupabaseDashboard

Creating a connection URL

Follow the steps outlined below to retrieve your database connection URL after your database has been successfully created. We'll need it to use Prisma in our Next.js app to query and create data.

  • Step1 : Head over to the Settings tab(Located at the left side)

Setting Tab

  • Step2 : Click the Database tab in the sidebar (Located on the left side)

Database

  • Step3 : Head over to the bottom of the page to find the Connection string section, then select Nodejs and copy the URL.

Connection string

Initializing Prisma

Prisma is a next-generation ORM that can be used in Node.js and TypeScript applications to access a database. W eare going to use prisma fo our application because it includes all of the code we need to run our queries. It will save us a lot of time and keep us from having to write a bunch of boilerplate codes.

Installing prisma

Prisma CLI installation

The Prisma command line interface (CLI) is the primary command-line interface for interacting with your Prisma project. It can create new project assets, generate Prisma Client, and analyze existing database structures via introspection to create your application models automatically.

npm i prisma
Enter fullscreen mode Exit fullscreen mode

Prisma Installation

Initialize prisma

Once you've installed the Prisma CLI, run the following command to get Prisma started in your Next.js application. It will then create a /prisma directory and the schema.prisma file within it inside your particular project folder. so, inside it we will be adding all the configuration for our application.

npx prisma init
Enter fullscreen mode Exit fullscreen mode

prismaSchema

// prisma.schema
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

Note: schema.prisma uses Prisma Schema Language(PSL)

Prisma-client-js, the Prisma JavaScript client, is the configured client represented by the generator block.

generator client {
  provider = "prisma-client-js"
}
Enter fullscreen mode Exit fullscreen mode

Next one is the the provider property of this block represents the type of database we want to use, and the connection url represents how Prisma connects to it.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

Environment variable

Using environment variables in the schema allows you to keep secrets out of the schema file which in turn improves the portability of the schema by allowing you to use it in different environments. Environment variables is created automatically after we fire the npx prisma init command.

Note: Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).

DATABASE_URL="postgresql://test:test@localhost:5432/test?schema=foo"
Enter fullscreen mode Exit fullscreen mode

As you can see, there is an DATABASE_URL variable with a dummy connection URL in this environment variable .env. So, replace this value with the connection string you obtained from Supabase.

DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.bboujxbwamqvgypibdkh.supabase.co:5432/postgres"
Enter fullscreen mode Exit fullscreen mode

Prisma schemas and models

We can begin working on our application's data models now that database is finally connected to your Next.js. In Prisma, our application models should be defined within the Prisma schema using the Prisma models. These models represent the entities of our application and are defined by the model blocks in the schema.prisma file. Each block contains several fields that represent the data for each entity. So, let's begin by creating the Product model, which will define the data schema for our products properties.

Defining models

Models represent the entities of your application domain. Models are represented by model blocks and define a number of fields. In this data model, Product is the model.

// prisma.schema
generator client {
  provider = "prisma-client-js"
}

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

model Product {
  id          String   @id @default(cuid())
  image       String?
  title       String
  description String
  status      String?
  price       Float
  authenticity        Int?
  returnPolicy        Int?
  warranty       Int?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

Each field, as shown in our Product model, has at least a name and its type. To learn more about the Scalar types and Prisma schema refrences visit the following links .

Generate Prisma Client

After designning Prisma model, we can begin generating our Prisma Client. We'll need to use Prisma's JavaScript library later in the article to interact with our data from within our Next.js app without having to write all of the SQL queries ourselves. But there's more to it. Prisma Client is, in fact, an auto-generated type-safe API designed specifically for our application which will gives us the JavaScript code we need to run queries on our data.

  • Step 1: Installing prisma client
  npm install @prisma/client
Enter fullscreen mode Exit fullscreen mode

PrismaClient

  • step2: Generating Prisma client
  npx prisma generate
Enter fullscreen mode Exit fullscreen mode

Prisma Generate

The @prisma/client npm package

The @prisma/client npm package consists of two key parts:

  • The @prisma/client module itself, which only changes when you re-install the package
  • The .prisma/client folder, which is the default location for the unique Prisma Client generated from your schema

@prisma/client/index.d.ts exports .prisma/client

Finally, after you have done that inside your ./node_modules folder, you should now find the generated Prisma Client code.

PrismaGenerate

Note: You need to re-run the prisma generate command after every change that's made to your Prisma schema to update the generated Prisma Client code.

Here is a graphical illustration of the typical workflow for the Prisma Client generation:

WorkFlow

Note also that prisma generate is automatically invoked when you're installing the @prisma/client npm package.

The Prisma Client is generated from the Prisma schema and is unique to your project. Each time you change the schema and run prisma generate, the client code changes itself.

PrismaClient

Pruning in Node.js package managers has no effect on the .prisma folder.

Creating a table in Supabase

If you look at your database in Supabase, you'll notice there is no table inside it. It's because we haven't yet created the Product table.

DashBoardScrrenshot

The Prisma model we defined in our schema.prisma file has not yet been reflected in our database. As a result, we must manually push changes to our data model to our database.

Pushing the data model

Prisma makes it really very easy to synchonize the schema with our database.So to do that follow the command listed below.

npx prisma db push
Enter fullscreen mode Exit fullscreen mode

PrismaDB Push

PushDatabase

This command is only good for prototyping on the schemas locally.

OR,

npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

This method (npx prisma migrate dev) will be used in this article because it is very useful in that it allows us to directly sync our Prisma schema with our database while also allowing us to easily track the changes that we make.

So, to begin using Prisma Migrate, enter the following command into the command prompt and after that enter a name for this first migration when prompted.

PrismaMigrate

After you have completed this process successfully, prisma will automatically generate SQL database migration files, and you should be able to see the SQL which should look something like this if you look inside the prisma folder.

FolderStructure

-- CreateTable
CREATE TABLE "Product" (
    "id" TEXT NOT NULL,
    "image" TEXT,
    "title" TEXT NOT NULL,
    "description" TEXT NOT NULL,
    "status" TEXT NOT NULL,
    "price" DOUBLE PRECISION NOT NULL,
    "authenticity" INTEGER,
    "returnPolicy" INTEGER,
    "warranty" INTEGER,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
---
Enter fullscreen mode Exit fullscreen mode

Finally, check the Supabase dashboard to see if everything has been successfully synced.

Supabase

Prisma Studio

Prisma Studio is a visual interface to the data residing inside your database where you can use to quickly visualize and manipulate the data. The cool thing about it is that it runs in entirely on your browser and you don't need to set up any connections because it's already comes with the prisma package. Not only that, from the studio, you can quickly open all of your application's models and interact with them directly via. studio itself.

Note: There is also desktop application available to download

Launching Prisma Studio

Launching the prisma studio is really very easy. Literally all you have to do is run the following command from a Prisma project.

npx prisma studio
Enter fullscreen mode Exit fullscreen mode

PrismaStudio

Now, open your browser and head over to http://localhost:5555/. You should be able to see the single table that we've created previously if you've followed all of the steps correctly.

PrismaStudio

PrismaStudio

Manually adding the records

Lets manually add some records and save the changes that we made.

PrismaStudio

DataFilled

Finally, lets create a functionality to access that data from within our Next.js app, where we can create new records, update existing ones, and delete old ones.

Interacting with data using Next.js

You should see some demo datas if you look at the Product page of your application.

LandingShopButton

Product Page

Now, open the file pages/products.js, file which represents our app's product page.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "products.json";

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, products data is comming from products.json file.

// products.json
[
  {
    "id": "001",
    "image": "/products/ballpen_300.png",
    "title": "Ball Pen",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 50
  },
  {
    "id": "002",
    "image": "/products/actioncamera_300.png",
    "title": "Go-pro cam",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 30
  },
  {
    "id": "003",
    "image": "/products/alarmclock_300.png",
    "title": "Alarm Clock",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 20
  },
  {
    "id": "004",
    "image": "/products/bangle_600.png",
    "title": "Bangle",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 2,
    "price": 200
  },
  {
    "id": "005",
    "image": "/products/bed_600.png",
    "title": "Large Bed",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "out of stock!",
    "warranty": 1,
    "price": 105
  },
  {
    "id": "006",
    "image": "/products/binderclip_600.png",
    "title": "Binder clip",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 2,
    "status": "new",
    "warranty": 1,
    "price": 2
  },
  {
    "id": "007",
    "image": "/products/beyblade_600.png",
    "title": "BeyBlade Burst",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "out of stock!",
    "warranty": 1,
    "price": 15
  },
  {
    "id": "008",
    "image": "/products/boxinggloves_600.png",
    "title": "Boxing gloves",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 2,
    "status": "new",
    "warranty": 1,
    "price": 45
  }
]
Enter fullscreen mode Exit fullscreen mode

This data & information is then passed as a prop from the Product component to the Grid component. The Grid component is then in charge of rendering those data as a grid of Card on the screen.

// Products.js
import PropTypes from "prop-types";
import Card from "@/components/Card";
import { ExclamationIcon } from "@heroicons/react/outline";

const Grid = ({ products = [] }) => {
  const isEmpty = products.length === 0;

  return isEmpty ? (
    <p className="text-purple-700 bg-amber-100 px-4 rounded-md py-2 max-w-max inline-flex items-center space-x-1">
      <ExclamationIcon className="shrink-0 w-5 h-5 mt-px" />
      <span>No data to be displayed.</span>
    </p>
  ) : (
    <div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
      {products.map((product) => (
        <Card key={product.id} {...product} onClickFavorite={toggleFavorite} />
      ))}
    </div>
  );
};

Grid.propTypes = {
  products: PropTypes.array,
};

export default Grid;
Enter fullscreen mode Exit fullscreen mode

Now we want to retrieve data from our database, and we'll do so using Server-Side Rendering (SSR). The ability of an application to convert HTML files on the server into a fully rendered HTML page for the client is known as server-side rendering (SSR). The web browser sends a request for information to the server, which responds immediately by sending the client a fully rendered page.

SSR

So, in order to use Server Side Rendering(SSR) with Next.js, we must export an asynchronous function getServerSideProps from within the file, which exports the page where we want to render out our data. The data returned by the getServerSideProps function will then be used by Next.js to pre-render our page on each individual request. Let's get started and export this function from our applicartion's Prodcuts page.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "products.json";

export async function getServerSideProps() {
  return {
    props: {
      // props for the Home component
    },
  };
}

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

To get the data from supabase, import and instantiate the generated Prisma client.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";
import { PrismaClient } from "@prisma/client";

import products from "products.json";

const prisma = new PrismaClient();

export async function getServerSideProps() {
  return {
    props: {
      // props for the Home component
    },
  };
}

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, Using the findMany query, we can get all of the records in our Product table:

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function getServerSideProps() {
  const products = await prisma.product.findMany();
  return {
    props: {
      products: JSON.parse(JSON.stringify(products)),
    },
  };
}

export default function Products({ products = [] }) {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Simply re-run the application, but if you get an error that looks like the one below, you'll need to regenerate the prisma and then re-run the server.

Error

As you can see, its fixed now

Fixed error

Finally, your application should resemble something like this:

Application Final Demno

Lets give users the functionality to actually create records from the application itself. So, first step is to actually create.

Create a new records

Head over to the pages/ folder and make a new file called addProduct.js.

Folder structure

// addProducts.js
import Layout from "@/components/Layout";
import ProductList from "@/components/ProductList";

const addProducts = () => {
  const createProduct = () => null;

  return (
    <Layout>
      <div className="max-w-screen-xl mx-auto flex-col">
        <h1 className="text-3xl font-medium text-gray-200 justify-center">
          Add your Products
        </h1>
        <div className="mt-8">
          <ProductList
            buttonText="Add Product"
            redirectPath="/products"
            onSubmit={createProduct}
          />
        </div>
      </div>
    </Layout>
  );
};

export default addProducts;
Enter fullscreen mode Exit fullscreen mode

After that head over to the ProductList component and make the following changes to that component.

//components/ProductList.js
import { useState } from "react";
import { useRouter } from "next/router";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { toast } from "react-hot-toast";
import { Formik, Form } from "formik";
import Input from "@/components/Input";
import AddProductImage from "@/components/AddProductImage";

const ProductSchema = Yup.object().shape({
  title: "Yup.string().trim().required(),"
  description: "Yup.string().trim().required(),"
  status: Yup.string().trim().required(),
  price: Yup.number().positive().integer().min(1).required(),
  authenticity: Yup.number().positive().integer().min(1).required(),
  returnPolicy: Yup.number().positive().integer().min(1).required(),
  warranty: Yup.number().positive().integer().min(1).required(),
});

const ProductList = ({
  initialValues = null,
  redirectPath = "",
  buttonText = "Submit",
  onSubmit = () => null,
}) => {
  const router = useRouter();

  const [disabled, setDisabled] = useState(false);
  const [imageUrl, setImageUrl] = useState(initialValues?.image ?? "");

  const upload = async (image) => {
    // TODO: Upload image to remote storage
  };

  const handleOnSubmit = async (values = null) => {
    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Submitting...");
      // Submit data
      if (typeof onSubmit === "function") {
        await onSubmit({ ...values, image: imageUrl });
      }
      toast.success("Successfully submitted", { id: toastId });
      // Redirect user
      if (redirectPath) {
        router.push(redirectPath);
      }
    } catch (e) {
      toast.error("Unable to submit", { id: toastId });
      setDisabled(false);
    }
  };

  const { image, ...initialFormValues } = initialValues ?? {
    image: "",
    title: "\"\","
    description: "\"\","
    status: "",
    price: 0,
    authenticity: 1,
    returnPolicy: 1,
    warranty: 1,
  };

  return (
    <div>
      <Formik
        initialValues={initialFormValues}
        validationSchema={ProductSchema}
        validateOnBlur={false}
        onSubmit={handleOnSubmit}
      >
        {({ isSubmitting, isValid }) => (
          <Form className="space-y-6">
            <div className="space-y-6">
              <Input
                name="title"
                type="text"
                label="Title"
                placeholder="Entire your product name..."
                disabled={disabled}
              />

              <Input
                name="description"
                type="textarea"
                label="Description"
                placeholder="Enter your product description...."
                disabled={disabled}
                rows={3}
              />

              <Input
                name="status"
                type="text"
                label="Status(new/out-of-stock/used)"
                placeholder="Enter your product status...."
                disabled={disabled}
              />

              <Input
                name="price"
                type="number"
                min="0"
                label="Price of the product..."
                placeholder="100"
                disabled={disabled}
              />

              <div className="justify-center">
                <Input
                  name="authenticity"
                  type="number"
                  min="0"
                  label="authenticity(%)"
                  placeholder="2"
                  disabled={disabled}
                />
                <Input
                  name="returnPolicy"
                  type="number"
                  min="0"
                  label="returnPolicy(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
                <Input
                  name="warranty"
                  type="number"
                  min="0"
                  label="warranty(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
              </div>
            </div>

            <div className="flex justify-center">
              <button
                type="submit"
                disabled={disabled || !isValid}
                className="bg-success text-white py-2 px-6 rounded-md focus:outline-none focus:ring-4 focus:ring-teal-600 focus:ring-opacity-50 hover:bg-teal-500 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-teal-600"
              >
                {isSubmitting ? "Submitting..." : buttonText}
              </button>
            </div>
          </Form>
        )}
      </Formik>
      <div className="mb-6 max-w-full">
        <AddProductImage
          initialImage={{ src: image, alt: initialFormValues.title }}
          onChangePicture={upload}
        />
      </div>
    </div>
  );
};

ProductList.propTypes = {
  initialValues: PropTypes.shape({
    image: PropTypes.string,
    title: "PropTypes.string,"
    description: "PropTypes.string,"
    status: PropTypes.string,
    price: PropTypes.number,
    authenticity: PropTypes.number,
    returnPolicy: PropTypes.number,
    warranty: PropTypes.number,
  }),
  redirectPath: PropTypes.string,
  buttonText: PropTypes.string,
  onSubmit: PropTypes.func,
};

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

After that, go to the AddProductImage file inside the component folder and copy the following code.

// AddProductImage.js
import { useState, useRef } from "react";
import PropTypes from "prop-types";
import Image from "next/image";
import toast from "react-hot-toast";
import classNames from "classnames";
import { CloudUploadIcon } from "@heroicons/react/outline";

const AddProductImage = ({
  label = "Image",
  initialImage = null,
  objectFit = "cover",
  accept = ".png, .jpg, .jpeg, .gif .jiff",
  sizeLimit = 10 * 1024 * 1024,
  onChangePicture = () => null,
}) => {
  const pictureRef = useRef();
  const [image, setImage] = useState(initialImage ?? null);
  const [updatingPicture, setUpdatingPicture] = useState(false);
  const [pictureError, setPictureError] = useState(null);

  const handleOnChangePicture = (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    const fileName = file?.name?.split(".")?.[0] ?? "New file";

    reader.addEventListener(
      "load",
      async function () {
        try {
          setImage({ src: reader.result, alt: fileName });
          if (typeof onChangePicture === "function") {
            await onChangePicture(reader.result);
          }
        } catch (err) {
          toast.error("Unable to update image");
        } finally {
          setUpdatingPicture(false);
        }
      },
      false
    );

    if (file) {
      if (file.size <= sizeLimit) {
        setUpdatingPicture(true);
        setPictureError("");
        reader.readAsDataURL(file);
      } else {
        setPictureError("File size is exceeding 10MB.");
      }
    }
  };

  const handleOnClickPicture = () => {
    if (pictureRef.current) {
      pictureRef.current.click();
    }
  };

  return (
    <div className="flex flex-col space-y-2">
      <label className="text-gray-200 ">{label}</label>

      <button
        disabled={updatingPicture}
        onClick={handleOnClickPicture}
        className={classNames(
          "relative aspect-video overflow-hidden rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition group focus:outline-none",
          image?.src
            ? "hover:opacity-50 disabled:hover:opacity-100"
            : "border-2 border-dotted hover:border-gray-400 focus:border-gray-400 disabled:hover:border-gray-200"
        )}
      >
        {image?.src ? (
          <Image
            src={image.src}
            alt={image?.alt ?? ""}
            layout="fill"
            objectFit={objectFit}
          />
        ) : null}

        <div className="flex items-center justify-center">
          {!image?.src ? (
            <div className="flex flex-col items-center space-y-2">
              <div className="shrink-0 rounded-full p-2 bg-gray-200 group-hover:scale-110 group-focus:scale-110 transition">
                <CloudUploadIcon className="w-4 h-4 text-gray-500 transition" />
              </div>
              <span className="text-xs font-semibold text-gray-500 transition">
                {updatingPicture
                  ? "Image Uploading..."
                  : "Upload product Image"}
              </span>
            </div>
          ) : null}
          <input
            ref={pictureRef}
            type="file"
            accept={accept}
            onChange={handleOnChangePicture}
            className="hidden"
          />
        </div>
      </button>

      {pictureError ? (
        <span className="text-red-600 text-sm">{pictureError}</span>
      ) : null}
    </div>
  );
};

AddProductImage.propTypes = {
  label: PropTypes.string,
  initialImage: PropTypes.shape({
    src: PropTypes.string,
    alt: PropTypes.string,
  }),
  objectFit: PropTypes.string,
  accept: PropTypes.string,
  sizeLimit: PropTypes.number,
  onChangePicture: PropTypes.func,
};

export default AddProductImage;
Enter fullscreen mode Exit fullscreen mode

This addProduct component renders the entire page's layout, which consist of a form from where you can add the product details and informations.

Demo

API endpoint

Let's actually create a API endpoint that will actually create a new record on our database via addProduct function.

const createProduct = () => null;
Enter fullscreen mode Exit fullscreen mode

But first, within our Next.js application project, let's create an API endpoint to handle our POST request for creating new records. Next.js provides a file based API routing so any file in the pages/api folder is mapped to /api/* and treated as an API endpoint rather than a page. They're only server-side bundles, so they won't add to the size of your client-side bundle. So, create a file name called products.js inside the pages/api folder and inside it create a request handler fucntion like shown below.

export default async function handler(req, res) {}
Enter fullscreen mode Exit fullscreen mode

Handling POST request for products

Before we go any further, use req.method to check the HTTP method of the request inside that request handler function. After that, return a 405 status code to the client becasue we are not handlling any kind of HTTP method.

// pages/api/products.js
export default async function handler(req, res) {
  if (req.method === "POST") {
    // TODO
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding new records with Prisma Client

Now, lets use Prisma Client to create a new Product record in the database using the data from the current HTTP request.

// pages/api/products.js
export default async function handler(req, res) {
  if (req.method === "POST") {
    const {
      image,
      title,
      description,
      status,
      price,
      authenticity,
      returnPolicy,
      warranty,
    } = req.body;
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

After that, lets actually initialize Prisma and call the create function that prisma provides.

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async function handler(req, res) {
  if (req.method === "POST") {
    const {
      image,
      title,
      description,
      status,
      price,
      authenticity,
      returnPolicy,
      warranty,
    } = req.body;

    const home = await prisma.product.create({
      data: {
        image,
        title,
        description,
        status,
        price,
        authenticity,
        returnPolicy,
        warranty,
      },
    });
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally lets add some try catch block to Handle the error.

// pages/api/products.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

export default async function handler(req, res) {
  if (req.method === "POST") {
    try {
      const {
        image,
        title,
        description,
        status,
        price,
        authenticity,
        returnPolicy,
        warranty,
      } = req.body;

      const product = await prisma.product.create({
        data: {
          image,
          title,
          description,
          status,
          price,
          authenticity,
          returnPolicy,
          warranty,
        },
      });
      res.status(200).json(product);
    } catch (e) {
      res.status(500).json({ message: "Something went wrong" });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

Now that we've created our API, let's call the API endpoint. To do so, open the addProduct.js file in the pages folder and make the following changes to the code, but first, we'll need to install the axios package, so do that first.

npm i axios
Enter fullscreen mode Exit fullscreen mode

OR

yarn add axios
Enter fullscreen mode Exit fullscreen mode

Axios

//pages/addProducts.js
import Layout from "@/components/Layout";
import ProductList from "@/components/ProductList";

const addProducts = () => {
  const createProduct = () => (data) => axios.post("/api/products", data);

  return (
    <Layout>
      <div className="max-w-screen-xl mx-auto flex-col">
        <h1 className="text-3xl font-medium text-gray-200 justify-center">
          Add your Products
        </h1>
        <div className="mt-8">
          <ProductList
            buttonText="Add Product"
            redirectPath="/products"
            onSubmit={createProduct}
          />
        </div>
      </div>
    </Layout>
  );
};

export default addProducts;
Enter fullscreen mode Exit fullscreen mode

Now lets re-run the server again.

image

After that head over to your browser and go to the http://localhost:3000/addProducts route and fill out all the product information and Submit it.

Demo
Demo
Demo

It will automatically redirect you to the /products page and you should be able to see the product that you just added.

Demo

Pre-rendering the pages

We've used the getServerSideProps function to pre-render the product of our app using Server-Side Rendering(SSR). Next.js, on the other hand, comes with a built-in pre-rendering method called Static Generation (SSG).

When a page uses Static Generation, the HTML for that page is generated during the build process. That means that when you run next build in production, the page HTML is generated. Each request will then be served with the same HTML. A CDN can cache it. You can statically generate pages with or without data using Next.js.

SSG

We can use different pre-rendering techniques on our applications when we use a framework like Next.js. For something more simple and non-dynamic, we can use static site generation(SSG). For dynamic content and more complex pages, we can use server-side rendering(SSR) .

Dynamic Routing with SSG

We can still statically generate pages with SSG after fetching some external data during the build process, even if SSG generates HTML at build time. learn more about static generation and dynamic routing.

Let's get data at build time by exporting an async function called getStaticProps from the pages we want to statically generate.

For Example,

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  );
}
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const res = await fetch("https://.../posts");
  const posts = await res.json();
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  };
}
export default Blog;
Enter fullscreen mode Exit fullscreen mode

Let's put Static Generation(SSG) to work in our application. The pages that render each individual Product listing are the ones that we'll statically generate at the build time. However, because product listings are generated through the users, we could end up with massive amount of pages. As a result, we won't be able to define those routes using predefined paths. Otherwise, we'll end up with a slew of useless files cluttering up our project.

We can easily create dynamic routes in Next.js. We just need to add brackets to a page's filename, [id].js, to create a dynamic route. However, in our project, we will place that in the Products folder. As a result, any route's ids will be matched with their specific id value, and the id value will be available inside the React component that renders the associated page.

Now, go to the pages folder and make a new folder called products, then make a new file called [id].js inside it.

products

And finally paste the following code inside that file.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export default ListedProducts;
Enter fullscreen mode Exit fullscreen mode

Now, let's actually provide the lists of paths of the pages that we want to statically generate, and let's actually fetch some data and match it with the numbers of paths. To do so, we must provide the paths to Next.js that we want to pre-render at build time.This function should return all the paths of the pages to pre-render at build time, along with the corresponding id value in the returned object's params property. So for that, we'll be using Prisma to retrieve the IDs for all of the products residing on our database.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export default ListedProducts;
Enter fullscreen mode Exit fullscreen mode

The getStaticProps function must now be implemented. So, let's get started. As you can see, the first thing we do is use the Prisma findUnique function with the id retrieved from the query params object to get the data of the requested route. Then, if the corresponding home is found in the database, we return it to the ListedProducts React component as a prop. If the requested products cannot be found, we return an object to tell Next.js to redirect the user to our app's 'products' page.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;
Enter fullscreen mode Exit fullscreen mode

Now re-run the server and head back to the browser and open the application.

Demo

Demo

Implementing Incremental Static Generation(ISR)

If you try to access a page for a new product listing in production, you'll get a 404 error page instead. To see this in action, build your app and run it as you would in production, because getStaticProps runs on every request in development. So, we have different behavior in development that differs from what we would see in production. To serve a production build of your application, simply fire up the following command, but make sure to stop the server first.

yarn build
Enter fullscreen mode Exit fullscreen mode

yarn build

yarn start
Enter fullscreen mode Exit fullscreen mode

yarn start

The main reason for the 404 page is that we used Static Generation to define the routes /products/[id].js, and we only generated pages for the products that were in our database at the time. In other words, after this build process, none of the products our users create will generate a new page. That is why we have a 404 page instead, because the page simply does not exist at all.To fix this, we'll need to define a fallback that will allow us to continue building pages lazily at runtime.

// pages/products/[id].js
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    // ----- SET to TRUE ------
    fallback: true,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;
Enter fullscreen mode Exit fullscreen mode

Now that we've set the fallback to true, the 404 page will no longer be displayed.

Fallback

It's also possible to detect whether the fallback version of the page is being rendered with the Next.js router and, if so, conditionally render something else, such as a loading spinner, while we wait for the props to get loaded.

const router = useRouter();

if (router.isFallback) {
  return (
    <svg
      role="status"
      class="mr-2 w-14 h-14 text-gray-200 animate-spin dark:text-gray-600 fill-success"
      viewBox="0 0 100 101"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
        fill="currentColor"
      />
      <path
        d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
        fill="currentFill"
      />
    </svg>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally your [id].js code should look something like this.

// pages/products/[id].js
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  const router = useRouter();

  if (router.isFallback) {
    return (
      <svg
        role="status"
        class="mr-2 w-14 h-14 text-gray-200 animate-spin dark:text-gray-600 fill-success"
        viewBox="0 0 100 101"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
          fill="currentColor"
        />
        <path
          d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
          fill="currentFill"
        />
      </svg>
    );
  }

  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;
Enter fullscreen mode Exit fullscreen mode

Uploading image in Supabase

We've created product records up to this point, but without any images because we haven't yet implemented aby media storage. We'll use Supabase Storage, a fantastic service from Supabase, to store and use media files in our project.

Creating a bucket in supabase

Buckets are distinct containers for files and folders. It is like a super folders. Generally you would create distinct buckets for different Security and Access Rules. For example, you might keep all public files in a public bucket, and other files that require logged-in access in a restricted bucket.

To create a bucket in Supabase, first navigate to the storage section of the dashboard.

Storage

After that, select Create Bucket button.

Storage

Next, give the bucket a name; for now, we'll call it supabase-ecommerce, and remember to make it public and click on that Create Button button.

Storage

Storage

Storage

Storage

Manually uploading image on database

  • Step 1: Head over to the supabase Storage and upload the products images.

Boxing
Boxing

Action Camera
Action Camera

Jwellery
Jwellery

  • Step 2: Select the product image and copy the image url

Boxing

Camera

Jwellery

  • Step 3: Open up the Prisma Studio by typing npx prisma studio inside the command line terminal.

Prisma Studio

Prisma

  • Step 3: Now, paste all of the image urls you copied in 'Step 2' inside the image row.

Image URL

Go back to the application and refresh the page now that you've added all of the image urls. You may encounter the error shown below.

Image URL

Copy the hostname of your file URL and paste it into the images.domains config in the next.config.js file to fix the error.

module.exports = {
  reactStrictMode: true,
  images: {
    domains: ["ezkjatblqzjynrebjkpq.supabase.co"],
  },
};
Enter fullscreen mode Exit fullscreen mode

After that, restart the server, and you should see images.

Demo
Demo
Demo

Security Rules

We must define some security rules to be able to deal with our image files inside our bucket using the Supabase API. So, add the security rules from our Supabase dashboard.

  • Step 1: Head over to the Storage section and go to the Policies section.

Security Rules

  • Step 2: Create a New Policy.

Security Rules

  • Step 3: Select Get started quickly.

Security Rules

  • Step 4: Use Allow access to JPG images in a public folder to anonymous users this template.

Security Rules

  • Step 5: Give the Policy Name select all the Operation and give bucket_id and Hit Review.

Security Rules

  • Step 6: Review the policy and save it.

Security Rules

Security Rules

  • Step 8: Finally you've successfully created a Storage Policy.

Security Rules

Upload a file from application

Let's keep going and add the ability for our application to upload and store our products images. Let's begin by adding a new API endpoint to your project's pages/api/productsImage.js directory.

// pages/api/productsImage.js
export default async function handler(req, res) {
  if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's use Supabase JS Client for uploading the image to our Supabase Storage Bucket.To do so, you need to install @supabase/supabase-js client library.

npm i @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

Supbase client

Then, inside your pages/api/productsImage.js file, import it and create a new Supabase Client.

// pages/api/productsImage.js
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_API_URL,
  process.env.SUPABASE_API_KEY
);
export default async function handler(req, res) {
  if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: not supported.` });
  }
}
Enter fullscreen mode Exit fullscreen mode

After that, go to the Supabase dashboard and click on Setting > API.

API key

and add all those API keys to your env file.

SUPABASE_API_KEY =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV6a2phdGJscXpqeW5yZWJ-";
SUPABASE_API_URL = "https://ezkjatblqzjynrebjkpq.supabase.co";
SUPABASE_STORAGE_BUCKET = "supabase-ecommerce";
Enter fullscreen mode Exit fullscreen mode

Now you need to add three packages to your application. The first one is base64-arraybuffer which encodes and decodes base64 to and from ArrayBuffers and another package called nanoid which is a very tiny, secure, URL-friendly, unique string ID generator for JavaScript.

yarn add nanoid base64-arraybuffer
Enter fullscreen mode Exit fullscreen mode

Supabase

Return to our API endpoint and upload a file to our bucket using the Supabase Client. Obtain the image data from the request's body and verify that it is not empty, then inspect the image data for Base64 encoding. After that, save the file to your Supbase storage bucket. With the SUPABASE_STORAGE_BUCKET env, you must provide the storage bucket name, the file path, and the decoded Base64 data, as well as the contentType. Once the image has been successfully uploaded, we can generate its public URL and return it to the client who initiated the HTTP request and then do some Error handling .So finally, your API endpoint for productsImage should look like this.

// pages/api/productsImage.js
import { supabase } from "@/lib/supabase";
import { nanoid } from "nanoid";
import { decode } from "base64-arraybuffer";

export default async function handler(req, res) {
  if (req.method === "POST") {
    let { image } = req.body;

    if (!image) {
      return res.status(500).json({ message: "There is no image" });
    }
    try {
      const imageType = image.match(/data:(.*);base64/)?.[1];
      const base64FileData = image.split("base64,")?.[1];
      if (!imageType || !base64FileData) {
        return res.status(500).json({ message: "Image data not valid" });
      }
      const fileName = nanoid();
      const ext = imageType.split("/")[1];
      const path = `${fileName}.${ext}`;
      const { data, error: uploadError } = await supabase.storage
        .from(process.env.SUPABASE_STORAGE_BUCKET)
        .upload(path, decode(base64FileData), {
          imageType,
          upsert: true,
        });
      if (uploadError) {
        console.log(uploadError);
        throw new Error("Image upload Failed!!");
      }
      const url = `${process.env.SUPABASE_API_URL.replace(
        ".co"
      )}/storage/v1/object/public/${data.Key}`;

      return res.status(200).json({ url });
    } catch (e) {
      res.status(500).json({ message: "Something went horribly wrong" });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: is not supported.` });
  }
}

export const config = {
  api: {
    bodyParser: {
      sizeLimit: "15mb",
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

After you have added the API endpoint make the following chnages to the ProductList.

import { useState } from "react";
import { useRouter } from "next/router";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { toast } from "react-hot-toast";
import { Formik, Form } from "formik";
import Input from "@/components/Input";
import AddProductImage from "@/components/AddProductImage";
import axios from "axios";

const ProductSchema = Yup.object().shape({
  title: Yup.string().trim().required(),
  description: Yup.string().trim().required(),
  status: Yup.string().trim().required(),
  price: Yup.number().positive().integer().min(1).required(),
  authenticity: Yup.number().positive().integer().min(1).required(),
  returnPolicy: Yup.number().positive().integer().min(1).required(),
  warranty: Yup.number().positive().integer().min(1).required(),
});

const ProductList = ({
  initialValues = null,
  redirectPath = "",
  buttonText = "Submit",
  onSubmit = () => null,
}) => {
  const router = useRouter();

  const [disabled, setDisabled] = useState(false);
  const [imageUrl, setImageUrl] = useState(initialValues?.image ?? "");

  const upload = async (image) => {
    if (!image) return;

    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Uploading...");
      const { data } = await axios.post("/api/productsImage", { image });
      setImageUrl(data?.url);
      toast.success("Successfully uploaded Image", { id: toastId });
    } catch (e) {
      toast.error("Unable to upload Image", { id: toastId });
      setImageUrl("");
    } finally {
      setDisabled(false);
    }
  };

  const handleOnSubmit = async (values = null) => {
    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Submitting...");
      // Submit data
      if (typeof onSubmit === "function") {
        await onSubmit({ ...values, image: imageUrl });
      }
      toast.success("Successfully submitted", { id: toastId });
      // Redirect user
      if (redirectPath) {
        router.push(redirectPath);
      }
    } catch (e) {
      toast.error("Unable to submit", { id: toastId });
      setDisabled(false);
    }
  };

  const { image, ...initialFormValues } = initialValues ?? {
    image: "",
    title: "",
    description: "",
    status: "",
    price: 0,
    authenticity: 1,
    returnPolicy: 1,
    warranty: 1,
  };

  return (
    <div>
      <Formik
        initialValues={initialFormValues}
        validationSchema={ProductSchema}
        validateOnBlur={false}
        onSubmit={handleOnSubmit}
      >
        {({ isSubmitting, isValid }) => (
          <Form className="space-y-6">
            <div className="space-y-6">
              <Input
                name="title"
                type="text"
                label="Title"
                placeholder="Entire your product name..."
                disabled={disabled}
              />

              <Input
                name="description"
                type="textarea"
                label="Description"
                placeholder="Enter your product description...."
                disabled={disabled}
                rows={3}
              />

              <Input
                name="status"
                type="text"
                label="Status(new/out-of-stock/used)"
                placeholder="Enter your product status...."
                disabled={disabled}
              />

              <Input
                name="price"
                type="number"
                min="0"
                label="Price of the product..."
                placeholder="100"
                disabled={disabled}
              />

              <div className="justify-center">
                <Input
                  name="authenticity"
                  type="number"
                  min="0"
                  label="authenticity(%)"
                  placeholder="2"
                  disabled={disabled}
                />
                <Input
                  name="returnPolicy"
                  type="number"
                  min="0"
                  label="returnPolicy(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
                <Input
                  name="warranty"
                  type="number"
                  min="0"
                  label="warranty(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
              </div>
            </div>

            <div className="flex justify-center">
              <button
                type="submit"
                disabled={disabled || !isValid}
                className="bg-success text-white py-2 px-6 rounded-md focus:outline-none focus:ring-4 focus:ring-teal-600 focus:ring-opacity-50 hover:bg-teal-500 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-teal-600"
              >
                {isSubmitting ? "Submitting..." : buttonText}
              </button>
            </div>
          </Form>
        )}
      </Formik>
      <div className="mb-6 max-w-full">
        <AddProductImage
          initialImage={{ src: image, alt: initialFormValues.title }}
          onChangePicture={upload}
        />
      </div>
    </div>
  );
};

ProductList.propTypes = {
  initialValues: PropTypes.shape({
    image: PropTypes.string,
    title: PropTypes.string,
    description: PropTypes.string,
    status: PropTypes.string,
    price: PropTypes.number,
    authenticity: PropTypes.number,
    returnPolicy: PropTypes.number,
    warranty: PropTypes.number,
  }),
  redirectPath: PropTypes.string,
  buttonText: PropTypes.string,
  onSubmit: PropTypes.func,
};

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

Now lets actually test our final application

Demo

Demo


Chatwoot Configuration

Chatwoot configuration on Heroku

Let's get started by creating a chatwoot instance on Heroku.

  • Step First: Create a free Heroku account by going to https://www.heroku.com/ and then going to the chatwoot GitHub repository and clicking the Deploy to Heroku button in the readme section.

Heroku

deploy

  • Step Second: After you click that button, you'll be able to see the basic setup that chatwoot has already completed. Give the App name and replace the FRONTEND_URL with the App name you just gave, then click Deploy App.

installation

URL config

  • Step Third: Depending on your PC, network status, and server location, the program may take 10 to 15 minutes to install.

deploying

  • Step Fourth: After the app has been deployed, go to the settings panel in the dashboard.

settings

  • Step Fifth: The domain section can be found in the settings menu. In a new window, open that URL. Finally, you've configured chatwoot in Heroku successfully.

domain

  • Step Sixth: Inside the Resources section, make sure the web and worker resources are enabled.

Resource section

  • Step Seventh: You should be able to log onto your chatwoot account if everything went smoothly.

login

So, your first account has been created successfully.The main benefit of deploying chatwoot on Heroku is that you have full control over your entire application and your entire data.

Chatwoot cloud setup

There is another way to get started with chatwoot which is the cloud way so this is the most straightforward way to get started is to register directly on the chatwoots website.

chatwoot

  • Step First: Fill out all of the required information to create an account.

Sign up

  • Step Second: You'll get an email asking you to confirm your account after you've signed up.

Account Confirm

  • Step Third: Proceed to login after you've confirmed your account by clicking the "Confirm my account" option.

Login

  • Step Fourth: You may now visit the Chatwoot dashboard and begin connecting it with plethora of platform (websites, Facebook, Twitter, etc.).

Chatwoot dashboard

Chatwoot Cloud Configuration
  • Step First: Let's set up an inbox. The inbox channel acts as a communication hub where everything can be managed, including live-chat, a Facebook page, a Twitter profile, email, and WhatsApp.

inbox channel

  • Step Second: Now, configure a website and domain name, as well as all of the heading and tagline information like shown below

Website Domain

  • Step Third: Finally, to control your mailbox, add "Agents." Keep in mind that only the "Agents" who have been authorized will have full access to the inbox.

agents

  • Step Fourth: Blammmm!. The website channel has been created successfully.

website channel code

The website channel must now be connected. Simply copy and paste the entire javascript code provided by chatwoot.Now, head back to our react app and create a new component folder and inside that folder create a new file/component called ChatwootWidget and inside it create a script which helps to loads the Chatwoot asynchronously. Simply follow the exact same steps outlined in the following code below.

// ChatwootWidget.js
import { useEffect } from "react";

const ChatwootWidget = () => {
  useEffect(() => {
    // Add Chatwoot Settings
    window.chatwootSettings = {
      hideMessageBubble: false,
      position: "right",
      locale: "en",
      type: "expanded_bubble",
    };

    (function (d, t) {
      var BASE_URL = "https://app.chatwoot.com";
      var g = d.createElement(t),
        s = d.getElementsByTagName(t)[0];
      g.src = BASE_URL + "/packs/js/sdk.js";
      g.defer = true;
      g.async = true;
      s.parentNode.insertBefore(g, s);
      g.onload = function () {
        window.chatwootSDK.run({
          websiteToken: ""// add you secret token here,
          baseUrl: BASE_URL,
        });
      };
    })(document, "script");
  }, []);

  return null;
};

export default ChatwootWidget;
Enter fullscreen mode Exit fullscreen mode

The best part about chatwoot is that you can customize it to your liking. For example, you can modify the position of the floating bubble, extend it, change the language, and hide the message bubble. All it takes is the addition of the following line of code.

window.chatwootSettings = {
  hideMessageBubble: false,
  position: "right",
  locale: "en",
  type: "expanded_bubble",
};
Enter fullscreen mode Exit fullscreen mode

Finally, it's time to import the ChatwootWidget component into our _app_.js file. To do so, simply navigate to the _app_.js file and import the chatwoot widget, then render that component. Your final code of _app_.js should look like this.

// _app.js.js
import "../styles/globals.css";
import { Toaster } from "react-hot-toast";
import ChatwootWidget from "@/components/ChatwootWidget";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Toaster />
      <ChatwootWidget />
    </>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Now that you've completed the chatwoot integration, your finished project should resemble something like this.

Demo

Demo

Deploying to netlify

First, sign in to netlify or create an account if you don't already have one.

Demo

You can also log in using a variety of other platforms.

Demo

Import your project from github now.

Demo

Sign-in and connect to your GitHub account.

Demo

Look for your project on Github.

Demo

Add all of the configuration, and don't forget to include the environment variables.

Demo

Yayyy!! šŸŽ‰ šŸŽ‰ Its deployed on Netlify!

Demoo

Conclusion

Congratulations šŸŽ‰ šŸŽ‰!!. You've successfully created a fullstack application with Next.js, Supabase, Prisma and chatwoot.This article may have been entertaining as well as instructive in terms of creating a fully fgledged working ecommerce site from absolute scratch.

Aviyel is a collaborative platform that assists open source project communities in monetizing and long-term sustainability. To know more visitĀ Aviyel.comĀ and find great blogs and events, just like this one! Sign up now for early access, and don't forget to follow us on our socials

Refrences

Top comments (2)

Collapse
 
ntubrian profile image
Brian Tzu-Pin Ho

Great sharing!

Collapse
 
gemcloud profile image
Gem Cloud

Great sharing! Thanks!!