Build a Modern Ecommerce Platform with Next.js, Tailwind CSS, Stripe, and Zustand

Pedro Machado / March 06, 2025
9 min read •
Description
Step-by-step tutorial for building a sleek, full‑stack ecommerce website featuring a dynamic product carousel, interactive product detail pages, and a secure Stripe checkout—all without a traditional backend database.
In this tutorial, you’ll learn how to build a modern ecommerce platform using Next.js 15, Tailwind CSS v4, Stripe, and Zustand for client‑side state management. This project focuses on creating a responsive frontend with interactive product pages, a live cart, and a secure checkout flow—all without using Prisma, Neon, or Postgres.
Table of Contents
Introduction
In this article, we will build a modern ecommerce platform with a dynamic product carousel, interactive product detail pages with cart controls, and a secure checkout process powered by Stripe. We’ll manage cart state with Zustand, and style our project using Tailwind CSS v4 with shadcn‑inspired components. This tutorial is perfect for developers who want to create a professional online store with the latest frontend technologies.
Tech Stack
- Next.js 15: Modern React framework featuring server components and the new app router.
- Tailwind CSS v4: Utility‑first CSS framework with a CSS‑first configuration.
- TypeScript: For type safety and modern development.
- Stripe: For product management and secure payment processing.
- Zustand: A lightweight state management solution for client‑side cart management.
Features
- Dynamic Product Carousel: A landing page carousel that auto‑cycles through featured products.
- Interactive Product Detail Pages: Detailed pages with plus and minus buttons to adjust cart quantities.
- Real‑Time Cart State: A live cart icon in the navbar displaying the current item count using Zustand.
- Secure Stripe Checkout: A streamlined checkout process powered by Stripe.
- Modern UI: Sleek, responsive design built with Tailwind CSS v4 and shadcn‑inspired components.
Quick Start
Prerequisites
Cloning the Repository
git clone https://github.com/yourusername/your-ecommerce-repo.git
cd your-ecommerce-repo
Installation
Install the dependencies:
npm install
Environment Variables
Create a .env
file in the project root and add the following:
STRIPE_SECRET_KEY=your-stripe-secret-key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key
NEXT_PUBLIC_BASE_URL=https://your-deployed-site.com
Running the Project
Start the development server:
npm run dev
Open http://localhost:3000 in your browser.
Code Walkthrough
Below is the code for all the components used in this project.
Root Layout
This file sets up the global layout, including the navbar, footer, and global styles.
// app/layout.tsx
import "../styles/globals.css";
import React from "react";
import Navbar from "../components/navbar";
import Footer from "../components/footer";
import { Toaster } from "@/components/ui/sonner"; // Example: shadcn Toaster component
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="h-full antialiased">
<head>
<title>My Ecommerce Platform</title>
</head>
<body className="flex min-h-full flex-col bg-white">
<Navbar />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
<Toaster position="top-center" offset={10} />
</body>
</html>
);
}
Navbar
The navbar includes a responsive menu, a cart icon that displays the total item count, and mobile toggling functionality.
// components/navbar.tsx
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ShoppingCart, Menu, X } from "lucide-react";
import { useCartStore } from "@/store/cart-store";
export default function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const { items } = useCartStore();
const cartCount = items.reduce((acc, item) => acc + item.quantity, 0);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) setMobileOpen(false);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<header className="sticky top-0 z-50 bg-white shadow">
<div className="container mx-auto flex items-center justify-between px-4 py-4">
<Link href="/">
<a className="text-xl font-bold hover:text-blue-600">My Ecommerce</a>
</Link>
<nav className="hidden md:flex space-x-6">
<Link href="/">
<a className="hover:text-blue-600">Home</a>
</Link>
<Link href="/products">
<a className="hover:text-blue-600">Products</a>
</Link>
<Link href="/checkout">
<a className="hover:text-blue-600">Checkout</a>
</Link>
</nav>
<div className="flex items-center space-x-4">
<Link href="/checkout">
<a className="relative">
<ShoppingCart className="h-6 w-6" />
{cartCount > 0 && (
<span className="absolute -top-2 -right-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
{cartCount}
</span>
)}
</a>
</Link>
<Button
variant="ghost"
className="md:hidden"
onClick={() => setMobileOpen(!mobileOpen)}
>
{mobileOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</Button>
</div>
</div>
{mobileOpen && (
<nav className="md:hidden bg-white shadow-md">
<ul className="flex flex-col p-4 space-y-2">
<li>
<Link href="/">
<a className="block hover:text-blue-600">Home</a>
</Link>
</li>
<li>
<Link href="/products">
<a className="block hover:text-blue-600">Products</a>
</Link>
</li>
<li>
<Link href="/checkout">
<a className="block hover:text-blue-600">Checkout</a>
</Link>
</li>
</ul>
</nav>
)}
</header>
);
}
Footer
A simple footer that displays at the bottom of the page.
// components/footer.tsx
import React from "react";
export default function Footer() {
return (
<footer className="bg-gray-100 py-6">
<div className="container mx-auto text-center text-sm text-gray-600">
© {new Date().getFullYear()} My Ecommerce. All rights reserved.
</div>
</footer>
);
}
Carousel
This component auto‑cycles through four featured products every 3 seconds, displaying each product’s image and price.
// components/carousel.tsx
"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import Stripe from "stripe";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
type CarouselProps = {
products: Stripe.Product[];
};
export default function Carousel({ products }: CarouselProps) {
const [current, setCurrent] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrent((prev) => (prev + 1) % products.length);
}, 3000);
return () => clearInterval(interval);
}, [products.length]);
const currentProduct = products[current];
const price = currentProduct.default_price as Stripe.Price;
return (
<Card className="relative overflow-hidden rounded-lg shadow-md">
{currentProduct.images && currentProduct.images[0] && (
<div className="relative h-80 w-full">
<Image
src={currentProduct.images[0]}
alt={currentProduct.name}
layout="fill"
objectFit="cover"
className="transition-opacity duration-500 ease-in-out"
/>
</div>
)}
<CardContent className="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-50">
<CardTitle className="text-3xl font-bold text-white mb-2">
{currentProduct.name}
</CardTitle>
{price && price.unit_amount && (
<p className="text-xl text-white">
${(price.unit_amount / 100).toFixed(2)}
</p>
)}
</CardContent>
</Card>
);
}
ProductCard
Displays a product in a card format with its image, title, description, and price. All cards are styled to have the same height.
// components/product-card.tsx
"use client";
import React from "react";
import Image from "next/image";
import Link from "next/link";
import Stripe from "stripe";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
type ProductCardProps = {
product: Stripe.Product;
};
export default function ProductCard({ product }: ProductCardProps) {
const price = product.default_price as Stripe.Price | null;
return (
<Link href={`/products/${product.id}`}>
<a className="block h-full">
<Card className="group hover:shadow-2xl transition duration-300 h-full flex flex-col">
{product.images && product.images[0] && (
<div className="relative h-60 w-full flex-shrink-0">
<Image
src={product.images[0]}
alt={product.name}
layout="fill"
objectFit="cover"
className="group-hover:opacity-90 transition-opacity duration-300 rounded-t-lg"
/>
</div>
)}
<CardHeader className="p-4 flex-shrink-0">
<CardTitle className="text-xl font-bold text-gray-800">
{product.name}
</CardTitle>
</CardHeader>
<CardContent className="p-4 flex-grow flex flex-col justify-between">
{product.description && (
<p className="text-gray-600 text-sm mb-2">
{product.description}
</p>
)}
{price && price.unit_amount && (
<p className="text-lg font-semibold text-gray-900">
${(price.unit_amount / 100).toFixed(2)}
</p>
)}
<Button className="mt-4">View Details</Button>
</CardContent>
</Card>
</a>
</Link>
);
}
ProductList
Lists all products in a responsive grid with an integrated search filter.
// components/product-list.tsx
"use client";
import React, { useState } from "react";
import ProductCard from "./product-card";
import Stripe from "stripe";
type ProductListProps = {
products: Stripe.Product[];
};
export default function ProductList({ products }: ProductListProps) {
const [searchTerm, setSearchTerm] = useState("");
const filteredProducts = products.filter((product) => {
const term = searchTerm.toLowerCase();
const nameMatch = product.name.toLowerCase().includes(term);
const descriptionMatch = product.description
? product.description.toLowerCase().includes(term)
: false;
return nameMatch || descriptionMatch;
});
return (
<div>
<div className="mb-6 flex justify-center">
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full max-w-md rounded border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<ul className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filteredProducts.map((product) => (
<li key={product.id} className="h-full">
<ProductCard product={product} />
</li>
))}
</ul>
</div>
);
}
ProductDetail
Displays detailed information about a single product along with plus and minus buttons to adjust cart quantity.
// components/product-detail.tsx
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { useCartStore } from "@/store/cart-store";
import Stripe from "stripe";
type ProductDetailProps = {
product: any; // Plain object from Stripe
};
export function ProductDetail({ product }: ProductDetailProps) {
const { addItem, incrementItem, decrementItem, items } = useCartStore();
const price = product.default_price?.unit_amount || 0;
const cartItem = items.find((item) => item.id === product.id);
const quantity = cartItem ? cartItem.quantity : 0;
return (
<div className="container mx-auto px-4 py-8 flex flex-col md:flex-row gap-8 items-center">
{product.images && product.images[0] && (
<div className="relative h-96 w-full md:w-1/2 rounded-lg overflow-hidden">
<Image
src={product.images[0]}
alt={product.name}
layout="fill"
objectFit="cover"
className="transition duration-300 hover:opacity-90"
/>
</div>
)}
<div className="md:w-1/2">
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
{product.description && (
<p className="text-gray-700 mb-4">{product.description}</p>
)}
<p className="text-2xl font-bold mb-4">${(price / 100).toFixed(2)}</p>
<div className="flex items-center space-x-4">
<Button variant="outline" onClick={() => decrementItem(product.id)}>
–
</Button>
<span className="text-lg font-semibold">{quantity}</span>
<Button
onClick={() => {
if (quantity === 0) {
addItem({
id: product.id,
name: product.name,
price,
imageUrl: product.images ? product.images[0] : null,
quantity: 1,
});
} else {
incrementItem(product.id);
}
}}
>
+
</Button>
</div>
</div>
</div>
);
}
export { ProductDetail };
Checkout Page
This page displays an order summary with plus/minus controls for each cart item and a form that triggers a server action to create a Stripe Checkout session. Upon successful payment, the cart will be cleared.
// app/checkout/page.tsx
"use client";
import React from "react";
import { useCartStore } from "@/store/cart-store";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { checkoutAction } from "./checkoutAction";
export default function CheckoutPage() {
const { items, clearCart, incrementItem, decrementItem } = useCartStore();
const total = items.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
if (items.length === 0) {
return (
<div className="container mx-auto px-4 py-8 text-center">
<h1 className="text-3xl font-bold mb-4">Your Cart is Empty</h1>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-center">Checkout</h1>
<Card className="max-w-md mx-auto mb-8">
<CardHeader>
<CardTitle className="text-xl font-bold">Order Summary</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-4">
{items.map((item) => (
<li key={item.id} className="flex flex-col gap-2 border-b pb-2">
<div className="flex justify-between">
<span className="font-medium">{item.name}</span>
<span className="font-semibold">
${((item.price * item.quantity) / 100).toFixed(2)}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => decrementItem(item.id)}
>
–
</Button>
<span className="text-lg font-semibold">{item.quantity}</span>
<Button
variant="outline"
size="sm"
onClick={() => incrementItem(item.id)}
>
+
</Button>
</div>
</li>
))}
</ul>
<div className="mt-4 border-t pt-2 text-lg font-semibold">
Total: ${(total / 100).toFixed(2)}
</div>
</CardContent>
</Card>
<form action={checkoutAction} method="POST" className="max-w-md mx-auto">
<input type="hidden" name="items" value={JSON.stringify(items)} />
<Button type="submit" variant="default" className="w-full">
Proceed to Payment
</Button>
</form>
</div>
);
}
Additional Resources
- Next.js Documentation
- Tailwind CSS v4 Docs
- Stripe API Reference
- Zustand GitHub Repository
- Watch the Full Tutorial on YouTube
Happy coding and enjoy building your modern ecommerce platform!