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
prisma
object from the@/app/lib/db/prisma
module, which likely provides a connection to the database using Prisma. - It imports the
ProductCard
component from the@/components/ProductCard
module. - The
Home
function is defined as anasync
function, which will be used as the content for the home page. - Inside the
Home
function:- It uses the
await
keyword to asynchronously fetch a list of products from the database using theprisma.product.findMany
method. TheorderBy
option is used to sort the products by theirid
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 theproduct
prop.
- 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
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.
- 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
formatPrice
function from the@/app/lib/format
module, which likely provides a way to format prices as currency strings. - 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 thespan
element.
- Inside the
PriceTag
component:- It uses the provided
formatPrice
function to format theprice
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 theclassName
prop.
- It uses the provided
- 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:
- 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.
- The
ProductCard
component is defined, which takes a single propproduct
of typeProduct
. - Inside the
ProductCard
component:- It uses the
Link
component to create a clickable link. The link points to a dynamic URL generated based on theid
property of theproduct
object. The link is wrapped around the entire card. - A
figure
element is used to contain anImage
component that displays the product's image. Thesrc
attribute is set to the product'simageUrl
, and width and height are specified for the image dimensions. TheclassName
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.
- 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
ProductCard
code with a few additional features. - 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), theisNew
variable 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
h2
element that displays the product name, a conditional rendering check is used:- If the
isNew
variable istrue
, adiv
with 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:
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.
- The
Home
function is defined as anasync
function, which will serve as the content for the home page. - Inside the
Home
function:- It fetches a list of products from the database using the
prisma.product.findMany
method. The products are ordered by theirid
in 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
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 theLink
component.
- 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. - The
key
prop is assigned to eachProductCard
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!