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:
- It imports the
prismaobject from the@/app/lib/db/prismamodule, which likely provides a connection to the database using Prisma. - It imports the
ProductCardcomponent from the@/components/ProductCardmodule. - The
Homefunction is defined as anasyncfunction, which will be used as the content for the home page. - Inside the
Homefunction:- It uses the
awaitkeyword to asynchronously fetch a list of products from the database using theprisma.product.findManymethod. TheorderByoption is used to sort the products by theiridproperty in descending order. - It wraps the fetched products in a
divelement. - It renders the
ProductCardcomponent, passing the first product from the fetched list as theproductprop.
- It uses the
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:
- 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). - Inside the function:
- It divides the
priceby 100 to convert it from cents to dollars. - It uses the
toLocaleStringmethod to format the converted price as a currency string. - The
en-USlocale is specified to ensure the formatting follows the conventions of the United States. - The
styleoption is set to"currency"to indicate that the value should be formatted as a currency. - The
currencyoption is set to"USD"to specify that the currency should be US dollars.
- It divides the
- 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:
- It imports the
formatPricefunction from the@/app/lib/formatmodule, which likely provides a way to format prices as currency strings. - The
PriceTagcomponent 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 thespanelement.
- Inside the
PriceTagcomponent:- It uses the provided
formatPricefunction to format thepriceprop into a currency string. - It wraps the formatted price inside a
spanelement 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 theclassNameprop.
- It uses the provided
- The component returns the
spanelement 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:
- It imports necessary modules and components:
Producttype from the@prisma/clientpackage.Linkcomponent from the "next/link" package for creating links.PriceTagcomponent (likely from a local file) for displaying formatted prices.Imagecomponent from the "next/image" package for displaying images with optimized loading and responsiveness.
- The
ProductCardcomponent is defined, which takes a single propproductof typeProduct. - Inside the
ProductCardcomponent:- It uses the
Linkcomponent to create a clickable link. The link points to a dynamic URL generated based on theidproperty of theproductobject. The link is wrapped around the entire card. - A
figureelement is used to contain anImagecomponent that displays the product's image. Thesrcattribute is set to the product'simageUrl, and width and height are specified for the image dimensions. TheclassNameis used to apply styling classes. - A
divwith the class "card-body" contains the following:- An
h2element with the class "card-title" displaying the product's name. - A
pelement displaying the product's description. - The
PriceTagcomponent is used to display the product's price, formatting it as a currency value.
- An
- It uses the
- 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:
- The code is very similar to the previous
ProductCardcode with a few additional features. - Inside the
ProductCardcomponent:- 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), theisNewvariable is set totrue, indicating that the product is new.
- It calculates whether the product is considered "new" based on the difference between the current date (
- After the
h2element that displays the product name, a conditional rendering check is used:- If the
isNewvariable istrue, adivwith the classes "badge" and "badge-accent" is rendered, displaying the text "NEW" as a badge.
- If the
- 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:
- The code imports required modules and components:
prismaobject from the@/app/lib/db/prismamodule for database access.ProductCardcomponent for displaying individual product cards.Imagecomponent from "next/image" for displaying optimized images.Linkcomponent from "next/link" for creating links.
- The
Homefunction is defined as anasyncfunction, which will serve as the content for the home page. - Inside the
Homefunction:- It fetches a list of products from the database using the
prisma.product.findManymethod. The products are ordered by theiridin descending order.
- It fetches a list of products from the database using the
- 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
Imagecomponent. The image source, dimensions, and styling classes are provided. - Next to the image, there's a
divwith the product name, description (sliced to 300 characters), and a "Check it out" link created using theLinkcomponent.
- 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
ProductCardcomponent is used to render each product card, passing the product data as a prop. - The
keyprop is assigned to eachProductCardcomponent 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!




