How to Display Products? (Part 3) : Building an E-Commerce Web App from Scratch with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB

Welcome to Part 3 of our blog series, "Building an E-Commerce Web App from Scratch." In this instalment, we're diving deep into creating a dynamic and captivating "Product List Page." Using the powerful combination of Prisma and MongoDB, we'll seamlessly display the products from your collection onto the home page.

Your online store is about to come to life as we leverage these technologies to create a user-friendly interface that showcases your products with flair. By the end of this post, you'll have an engaging home page that not only looks stunning but also provides your customers with an immersive shopping experience.


As we continue to build upon the foundation set in the previous parts, it's exciting to see your E-Commerce Web App take shape. The seamless integration of Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB ensures that your platform remains both visually appealing and highly functional.

Get ready to elevate your online store's appeal and provide your customers with a seamless shopping journey. Let's dive into the world of dynamic product displays and make your E-Commerce Web App an irresistible destination for shoppers.

Tutorial

This is a continuation to Add Products to Database Using NextJS Server Actions (Part 2) : Building an E-Commerce Web App from Scratch with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB. I would strongly recommend reading through Part 2 to understand how we added products to the MongoDB database which we will use in this part to fetch and display on the home page of our ecommerce web app.

To begin displaying products on the home page, we will first go to the page.tsx within the app directory, clear off the existing code, and replace it with some starter code for now like this:

import { prisma } from "@/app/lib/db/prisma";

export default async function Home() {
  const products = await prisma.product.findMany({
    orderBy: {
      id: "desc",
    },
  });
  return 
  (
    <div>
      
    </div>
  )
}

Building the Product Card

Next, we need to display the product details on a card which we will reuse at multiple places. You can refer to this for Daisy UI Card component which we will be using: Tailwind Card Component — Tailwind CSS Components (daisyui.com)

We will create a ProductCard.tsx file in the previously created components folder within the src directory with the following code:

import { Product } from "@prisma/client";
import Link from "next/link";

interface ProductCardProps {
  product: Product;
}

export default function ProductCard({ product }: ProductCardProps) {
  return (
    <Link
      href={"/products/" + product.id}
      className="card w-full bg-base-100 hover:shadow-xl transition-shadow"
    >
      <div className="card-body">
        <h2 className="card-title">{product.name}</h2>
        <p>{product.description}</p>
      </div>
    </Link>
  );
}

The above code defines a React component called ProductCard that takes a single prop named product. The product prop is expected to be an object conforming to the structure of the Product interface from the @prisma/client package.

Inside the component, it uses the Link component from the "next/link" package to create a clickable link. This link points to a dynamic URL generated based on the id property of the product object. The link is wrapped around a div element that represents a card-like UI element.

The card's appearance is controlled through CSS classes like "card", "w-full", "bg-base-100", "hover:shadow-xl", and "transition-shadow". These classes likely style the card's width, background color, shadow effect on hover, and transitions.

Within the card body, the product's name and description are displayed using the product prop's name and description properties, respectively. The name is displayed as an h2 element with the class "card-title", while the description is displayed as a p element.

In summary, the code defines a reusable React component for displaying a product card with a clickable link to the product's detailed page. The card contains the product's name and description, and its appearance is enhanced with CSS classes for styling and hover effects.

For testing purposes, let’s just fetch one product from the list and see how it works in the page.tsx within the app directory like this:

import { prisma } from "@/app/lib/db/prisma";
import ProductCard from "@/components/ProductCard";

export default async function Home() {
  const products = await prisma.product.findMany({
    orderBy: {
      id: "desc",
    },
  });
  return (
    <div>
      <ProductCard product={products[0]} />
    </div>
  );
}

The above code is part of a Next.js application and serves as the logic for the home page. Here's a brief explanation of what it does:

  1. It imports the prisma object from the @/app/lib/db/prisma module, which likely provides a connection to the database using Prisma.
  2. It imports the ProductCard component from the @/components/ProductCard module.
  3. The Home function is defined as an async function, which will be used as the content for the home page.
  4. Inside the Home function:
    • It uses the await keyword to asynchronously fetch a list of products from the database using the prisma.product.findMany method. The orderBy option is used to sort the products by their id property in descending order.
    • It wraps the fetched products in a div element.
    • It renders the ProductCard component, passing the first product from the fetched list as the product prop.

In summary, the Home function fetches a list of products from the database and displays the first product using the ProductCard component. This code is responsible for populating the home page of the application with product information and its corresponding card component.

Formatting Price for Price Tag

Now, we’ll need a function that will help us handle the formatting of the price that we’ve entered in our database. For that, we will create a format.ts file in the lib folder with the following logic:

export function formatPrice(price: number) {
  return (price / 100).toLocaleString("en-US", {
    style: "currency",
    currency: "USD",
  });
}

The above code defines a utility function named formatPrice that takes a numeric value representing a price in cents. The purpose of this function is to convert the price from cents to a formatted currency string in US dollars (USD). Here's what the function does:

  1. The function takes a single argument price, which is expected to be a number representing a price in cents (e.g., 100 cents for $1.00).
  2. Inside the function:
    • It divides the price by 100 to convert it from cents to dollars.
    • It uses the toLocaleString method to format the converted price as a currency string.
    • The en-US locale is specified to ensure the formatting follows the conventions of the United States.
    • The style option is set to "currency" to indicate that the value should be formatted as a currency.
    • The currency option is set to "USD" to specify that the currency should be US dollars.
  3. The function returns the formatted currency string representing the price in US dollars.

In summary, the formatPrice function takes a price value in cents, converts it to dollars, and then formats it as a currency string in US dollars using the toLocaleString method.

After this, we will create a PriceTag.tsx file in the components which we will use to display the price tag within the product card like this:

import { formatPrice } from "@/app/lib/format";

interface PriceTagProps {
  price: number;
  className?: string;
}

export default function PriceTag({ price, className }: PriceTagProps) {
  return (
    <span className={`badge badge-lg ${className}`}>{formatPrice(price)}</span>
  );
}

The above code defines a React component called PriceTag that displays a formatted price in a badge-like style. Here's a brief explanation of what the code does:

  1. It imports the formatPrice function from the @/app/lib/format module, which likely provides a way to format prices as currency strings.
  2. The PriceTag component is defined, which takes two props:
    • price: A number representing the price to be displayed.
    • className (optional): A string representing CSS classes to be applied to the span element.
  3. Inside the PriceTag component:
    • It uses the provided formatPrice function to format the price prop into a currency string.
    • It wraps the formatted price inside a span element with a class composed of "badge" (likely for styling purposes from Daisy UI Tailwind Badge Component — Tailwind CSS Components (daisyui.com)) and any additional classes specified through the className prop.
  4. The component returns the span element with the formatted price inside, along with the specified CSS classes.

In summary, the PriceTag component takes a numeric price prop, formats it using the formatPrice function, and displays it within a span element styled with a "badge" class and any additional classes provided through the className prop. This component is useful for displaying prices with consistent formatting and styling across the application.

Now, we will import the price tag and add the product image to the card by modifying the ProductCard.tsx in the following way:

import { Product } from "@prisma/client";
import Link from "next/link";
import PriceTag from "./PriceTag";
import Image from "next/image";

interface ProductCardProps {
  product: Product;
}

export default function ProductCard({ product }: ProductCardProps) {
  return (
    <Link
      href={"/products/" + product.id}
      className="card w-full bg-base-100 hover:shadow-xl transition-shadow"
    >
      <figure>
        <Image
          src={product.imageUrl}
          alt={product.name}
          width={800}
          height={400}
          className="h-48 object-cover"
        />
      </figure>
      <div className="card-body">
        <h2 className="card-title">{product.name}</h2>
        <p>{product.description}</p>
        <PriceTag price={product.price} />
      </div>
    </Link>
  );
}

The above code defines a React component called ProductCard that displays information about a product in a card-like format. Here's an explanation of the code:

  1. It imports necessary modules and components:
    • Product type from the @prisma/client package.
    • Link component from the "next/link" package for creating links.
    • PriceTag component (likely from a local file) for displaying formatted prices.
    • Image component from the "next/image" package for displaying images with optimized loading and responsiveness.
  2. The ProductCard component is defined, which takes a single prop product of type Product.
  3. Inside the ProductCard component:
    • It uses the Link component to create a clickable link. The link points to a dynamic URL generated based on the id property of the product object. The link is wrapped around the entire card.
    • A figure element is used to contain an Image component that displays the product's image. The src attribute is set to the product's imageUrl, and width and height are specified for the image dimensions. The className is used to apply styling classes.
    • A div with the class "card-body" contains the following:
      • An h2 element with the class "card-title" displaying the product's name.
      • A p element displaying the product's description.
      • The PriceTag component is used to display the product's price, formatting it as a currency value.
  4. The component returns the entire structure, including the link, image, product information, and price.

In summary, the ProductCard component is used to display detailed information about a product in a card format. It includes an image, name, description, and price, all wrapped in a link for navigation. The image is displayed using the optimized Image component, and the price is formatted using the PriceTag component.

Once, this is saved, we should be able to see our product card on the home page like this:


Now, we will add a NEW tag to products which will be created within the last 7 days by modifying the ProductCard.tsx file in the following way:

import { Product } from "@prisma/client";
import Link from "next/link";
import PriceTag from "./PriceTag";
import Image from "next/image";

interface ProductCardProps {
  product: Product;
}

export default function ProductCard({ product }: ProductCardProps) {
  const isNew =
    Date.now() - new Date(product.createdAt).getTime() <
    1000 * 60 * 60 * 24 * 7;

  return (
    <Link
      href={"/products/" + product.id}
      className="card w-full bg-base-100 hover:shadow-xl transition-shadow"
    >
      <figure>
        <Image
          src={product.imageUrl}
          alt={product.name}
          width={800}
          height={400}
          className="h-64 object-cover"
        />
      </figure>
      <div className="card-body">
        <h2 className="card-title">{product.name}</h2>
        {isNew && <div className="badge badge-accent">NEW</div>}
        <p>{product.description.slice(0, 100) + "..."}</p>
        <PriceTag price={product.price} />
      </div>
    </Link>
  );
}

The above code is an enhanced version of the previous ProductCard component that includes a "NEW" badge for products created within the last week. Here's what this version does:

  1. The code is very similar to the previous ProductCard code with a few additional features.
  2. Inside the ProductCard component:
    • It calculates whether the product is considered "new" based on the difference between the current date (Date.now()) and the creation date of the product (product.createdAt). If the product's creation date is less than 7 days ago (in milliseconds), the isNew variable is set to true, indicating that the product is new.
  3. After the h2 element that displays the product name, a conditional rendering check is used:
    • If the isNew variable is true, a div with the classes "badge" and "badge-accent" is rendered, displaying the text "NEW" as a badge.
  4. The rest of the component structure remains the same, including displaying the product's image, description, and price.

In summary, this enhanced version of the ProductCard component adds a feature to display a "NEW" badge for products that were created within the last week. The badge is conditionally displayed after the product name, providing a visual indication to users about new products.


Displaying Hero Product & All Products

Now we will modify the page.tsx within the app directory in the following way to display a large hero product or featured product at the top using Daisy Hero UI and a grid of product cards under it like this:

import { prisma } from "@/app/lib/db/prisma";
import ProductCard from "@/components/ProductCard";
import Image from "next/image";
import Link from "next/link";
export default async function Home() {
  const products = await prisma.product.findMany({
    orderBy: {
      id: "desc",
    },
  });
  return (
    <div>
      <div className="hero rounded-xl bg-base-200">
        <div className="hero-content flex-col lg:flex-row">
          <Image
            src={products[0].imageUrl}
            alt={products[0].name}
            width={400}
            height={400}
            className="w-full h-64 object-cover max-w-sm sm:max-w-2xl rounded-lg shadow-2xl"
            priority
          />
          <div>
            <h1 className="text-5xl font-bold">{products[0].name}</h1>
            <p className="py-6">
              {products[0].description.slice(0, 200) + "..."}
            </p>
            <Link
              href={"/products/" + products[0].id}
              className="btn btn-primary"
            >
              Check it out
            </Link>
          </div>
        </div>
      </div>

      <div className="my-4 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
        {products.slice(1).map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

The above code defines the logic for the home page of a Next.js application that displays a hero section with the most recent product and a grid of other product cards. Here's a breakdown of what the code does:

  1. The code imports required modules and components:
    • prisma object from the @/app/lib/db/prisma module for database access.
    • ProductCard component for displaying individual product cards.
    • Image component from "next/image" for displaying optimized images.
    • Link component from "next/link" for creating links.
  2. The Home function is defined as an async function, which will serve as the content for the home page.
  3. Inside the Home function:
    • It fetches a list of products from the database using the prisma.product.findMany method. The products are ordered by their id in descending order.
  4. The component returns a structure that includes:
    • A hero section with a rounded background using the class "hero" and "bg-base-200".
    • Inside the hero section, there's a flex layout with an image displayed using the Image component. The image source, dimensions, and styling classes are provided.
    • Next to the image, there's a div with the product name, description (sliced to 300 characters), and a "Check it out" link created using the Link component.
  5. Below the hero section, a grid layout is created using CSS classes to display the remaining products (excluding the first one) as product cards. The ProductCard component is used to render each product card, passing the product data as a prop.
  6. The key prop is assigned to each ProductCard component to help React efficiently manage the components when they are updated.

In summary, the Home function fetches a list of products and displays them on the home page. The first product is highlighted in a hero section with an image and a link. The remaining products are displayed in a grid format using the ProductCard component.

This will create a responsive grid of products which will look something like this:


Once, this is done, we will add a couple of more products to the MongoDB database that we created in Part 2. You may add the following sample data or create your own.

{
  "name": "Premium Over-Ear Headphones",
  "description": "Immerse yourself in exceptional sound quality with these Premium Over-Ear Headphones. Designed for music lovers and audiophiles, they deliver rich, detailed audio across all genres. With noise isolation technology, you can enjoy your music without distractions. These headphones offer a comfortable fit with cushioned ear cups and an adjustable headband. The foldable design makes them travel-friendly, while the included carrying case ensures they stay protected on the go. Featuring Bluetooth connectivity, you can listen wirelessly from your device with ease. The built-in microphone allows for hands-free calls, and the on-ear controls make adjusting volume and playback a breeze. Whether you're enjoying music at home or on the move, these Premium Over-Ear Headphones provide a listening experience that's second to none.",
  "image_url": "<https://images.unsplash.com/photo-1546435770-a3e426bf472b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1165&q=80>",
  "price": 14999
}
{
  "name": "Classic Aviator Sunglasses",
  "description": "Elevate your style with these Classic Aviator Sunglasses. Timeless and versatile, they blend fashion with UV protection. The iconic aviator design features a metal frame and tinted lenses that shield your eyes from harmful rays. Crafted for comfort, these sunglasses have adjustable nose pads and lightweight construction. Whether you're strolling on the beach or exploring the city, they offer a perfect fit and superior clarity. The sunglasses come with a sleek protective case and cleaning cloth. With their understated elegance and reliable sun protection, these Classic Aviator Sunglasses are a must-have accessory for any sunny day.",
  "image_url": "<https://images.unsplash.com/photo-1511499767150-a48a237f0083?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=880&q=80>",
  "price": 8999
}
{
  "name": "Elegant Classic Wristwatch",
  "description": "Adorn your wrist with timeless sophistication with this Elegant Classic Wristwatch. A fusion of style and functionality, it complements any attire. The watch features a polished stainless steel case, a refined dial, and a genuine leather strap.Powered by precise quartz movement, this wristwatch ensures accurate timekeeping. The minimalist design showcases hour markers, slim hands, and a date display for easy readability. Its water-resistant construction offers durability in everyday wear. With its adjustable buckle closure, the genuine leather strap offers a comfortable fit for any wrist size. Whether you're attending a formal event or embracing casual elegance, the Elegant Classic Wristwatch is the perfect accessory to enhance your look.",
  "image_url": "<https://images.unsplash.com/photo-1623998021450-85c29c644e0d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=957&q=80>",
  "price": 12999
}

Once this is added, we can view a responsive grid of products in the following way:


Conclusion to Part 3

Congratulations on reaching the conclusion of Part 3 in our series, "Building an E-Commerce Web App from Scratch." By leveraging the power of Prisma and MongoDB, we've created a captivating "Product List Page" that seamlessly displays your products on the home page, enhancing the shopping experience for your customers.

As your online store gains momentum, it's thrilling to witness the transformation from concepts to reality. The synergy of Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB continues to play a pivotal role in both the aesthetics and functionality of your E-Commerce Web App.

But our journey is far from over! In Part 4, get ready to delve into the intricacies of a "Product Details Page." Here, we'll harness the potential of Next.js to dynamically retrieve parameters from the home page, allowing us to display unique and detailed information for each product.

We're excited to guide you through this process, ensuring that you have all the tools you need to create a personalized and engaging product presentation. Stay tuned as we continue to unravel the possibilities of building a top-notch E-Commerce Web App that stands out in the digital landscape.

Thank you for being a part of this journey. In Part 4, we'll take another step towards creating an exceptional shopping experience for your customers. Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript