Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Project Ecommerce Catalog
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Project 2: E-commerce Product Catalog

Advanced filtering, search, and shopping cart

Build a production-ready product catalog with advanced features! Implement URL-based filtering for shareable links, real-time search, multi-select filters, price ranges, sorting options, pagination, and a shopping cart with persistent state. This project demonstrates complex data filtering, state management, and building real e-commerce functionality!

Project Overview

Features We'll Build

  • Product Listing: Grid view with pagination
  • Advanced Filtering: Category, price range, ratings, brand
  • Search: Real-time product search
  • Sorting: Price, name, rating, newest
  • Product Details: Individual product pages
  • Shopping Cart: Add, remove, update quantities
  • Cart Persistence: Save cart to localStorage
  • URL State: Shareable filtered URLs
  • Responsive Design: Mobile-friendly layout
  • Loading States: Suspense and skeleton loaders

Technologies Used

  • Next.js 15 with App Router
  • TypeScript for type safety
  • Zustand for cart state management
  • Prisma or JSON for product data
  • Tailwind CSS for styling
  • searchParams for URL-based filters

Data Structure

Product Type Definition

lib/types.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  originalPrice?: number; // For showing discounts
  category: string;
  brand: string;
  image: string;
  images: string[]; // Multiple product images
  rating: number; // 0-5
  reviewCount: number;
  inStock: boolean;
  stockCount: number;
  tags: string[];
  specifications: Record<string, string>;
  createdAt: string;
}

export interface FilterParams {
  search?: string;
  category?: string;
  brand?: string[];
  minPrice?: number;
  maxPrice?: number;
  minRating?: number;
  inStock?: boolean;
  sortBy?: 'price-asc' | 'price-desc' | 'name-asc' | 'name-desc' | 'rating' | 'newest';
  page?: number;
  perPage?: number;
}

export interface PaginatedProducts {
  products: Product[];
  total: number;
  page: number;
  perPage: number;
  totalPages: number;
}

// ✅ Complete product structure
// ✅ Flexible filtering options
// ✅ Pagination support
// ✅ Type-safe throughout

Mock Product Data

data/products.json
[
  {
    "id": "1",
    "name": "Wireless Headphones",
    "description": "Premium noise-cancelling wireless headphones with 30-hour battery life.",
    "price": 299.99,
    "originalPrice": 399.99,
    "category": "Electronics",
    "brand": "AudioTech",
    "image": "/images/headphones-1.jpg",
    "images": [
      "/images/headphones-1.jpg",
      "/images/headphones-2.jpg",
      "/images/headphones-3.jpg"
    ],
    "rating": 4.5,
    "reviewCount": 128,
    "inStock": true,
    "stockCount": 45,
    "tags": ["wireless", "noise-cancelling", "bluetooth"],
    "specifications": {
      "Battery Life": "30 hours",
      "Connectivity": "Bluetooth 5.0",
      "Weight": "250g",
      "Color": "Black"
    },
    "createdAt": "2024-01-15T10:00:00Z"
  },
  {
    "id": "2",
    "name": "Smart Watch Pro",
    "description": "Advanced fitness tracking with heart rate monitor and GPS.",
    "price": 449.99,
    "category": "Electronics",
    "brand": "TechWear",
    "image": "/images/watch-1.jpg",
    "images": ["/images/watch-1.jpg", "/images/watch-2.jpg"],
    "rating": 4.7,
    "reviewCount": 89,
    "inStock": true,
    "stockCount": 23,
    "tags": ["smartwatch", "fitness", "gps"],
    "specifications": {
      "Display": "1.4 inch AMOLED",
      "Battery": "7 days",
      "Water Resistant": "5ATM",
      "Sensors": "Heart Rate, GPS, SpO2"
    },
    "createdAt": "2024-01-20T14:30:00Z"
  }
]

// ✅ Rich product data
// ✅ Multiple images
// ✅ Specifications
// ✅ Stock tracking

E-commerce Project Structure

Complete file organization for product catalog

project-rootImportant
app
lib
data

Select a file or folder to see details

Product Utilities

Product Filtering and Search

lib/products.ts
import productsData from '@/data/products.json';
import { Product, FilterParams, PaginatedProducts } from './types';

const products = productsData as Product[];

export function getProducts(filters: FilterParams = {}): PaginatedProducts {
  let filtered = [...products];
  
  // Search filter
  if (filters.search) {
    const query = filters.search.toLowerCase();
    filtered = filtered.filter(
      (p) =>
        p.name.toLowerCase().includes(query) ||
        p.description.toLowerCase().includes(query) ||
        p.tags.some((tag) => tag.toLowerCase().includes(query))
    );
  }
  
  // Category filter
  if (filters.category) {
    filtered = filtered.filter((p) => p.category === filters.category);
  }
  
  // Brand filter (multi-select)
  if (filters.brand && filters.brand.length > 0) {
    filtered = filtered.filter((p) => filters.brand!.includes(p.brand));
  }
  
  // Price range filter
  if (filters.minPrice !== undefined) {
    filtered = filtered.filter((p) => p.price >= filters.minPrice!);
  }
  if (filters.maxPrice !== undefined) {
    filtered = filtered.filter((p) => p.price <= filters.maxPrice!);
  }
  
  // Rating filter
  if (filters.minRating !== undefined) {
    filtered = filtered.filter((p) => p.rating >= filters.minRating!);
  }
  
  // Stock filter
  if (filters.inStock) {
    filtered = filtered.filter((p) => p.inStock && p.stockCount > 0);
  }
  
  // Sorting
  const sortBy = filters.sortBy || 'newest';
  switch (sortBy) {
    case 'price-asc':
      filtered.sort((a, b) => a.price - b.price);
      break;
    case 'price-desc':
      filtered.sort((a, b) => b.price - a.price);
      break;
    case 'name-asc':
      filtered.sort((a, b) => a.name.localeCompare(b.name));
      break;
    case 'name-desc':
      filtered.sort((a, b) => b.name.localeCompare(a.name));
      break;
    case 'rating':
      filtered.sort((a, b) => b.rating - a.rating);
      break;
    case 'newest':
      filtered.sort(
        (a, b) =>
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
      );
      break;
  }
  
  // Pagination
  const page = filters.page || 1;
  const perPage = filters.perPage || 12;
  const total = filtered.length;
  const totalPages = Math.ceil(total / perPage);
  const start = (page - 1) * perPage;
  const end = start + perPage;
  const paginatedProducts = filtered.slice(start, end);
  
  return {
    products: paginatedProducts,
    total,
    page,
    perPage,
    totalPages,
  };
}

export function getProductById(id: string): Product | undefined {
  return products.find((p) => p.id === id);
}

export function getCategories(): string[] {
  const categories = new Set(products.map((p) => p.category));
  return Array.from(categories).sort();
}

export function getBrands(): string[] {
  const brands = new Set(products.map((p) => p.brand));
  return Array.from(brands).sort();
}

export function getPriceRange(): { min: number; max: number } {
  const prices = products.map((p) => p.price);
  return {
    min: Math.floor(Math.min(...prices)),
    max: Math.ceil(Math.max(...prices)),
  };
}

// ✅ Comprehensive filtering
// ✅ Multi-criteria search
// ✅ Flexible sorting
// ✅ Pagination built-in
// ✅ Helper functions for filters

Product Listing Page

Products Page with Filters

app/products/page.tsx
import { Suspense } from 'react';
import { getProducts, getCategories, getBrands, getPriceRange } from '@/lib/products';
import { ProductGrid } from '@/components/ProductGrid';
import { ProductFilters } from '@/components/ProductFilters';
import { ProductSort } from '@/components/ProductSort';
import { Pagination } from '@/components/Pagination';
import { FilterParams } from '@/lib/types';

interface PageProps {
  searchParams: {
    search?: string;
    category?: string;
    brand?: string | string[];
    minPrice?: string;
    maxPrice?: string;
    minRating?: string;
    inStock?: string;
    sortBy?: string;
    page?: string;
  };
}

export default function ProductsPage({ searchParams }: PageProps) {
  // Parse filters from searchParams
  const filters: FilterParams = {
    search: searchParams.search,
    category: searchParams.category,
    brand: Array.isArray(searchParams.brand)
      ? searchParams.brand
      : searchParams.brand
      ? [searchParams.brand]
      : undefined,
    minPrice: searchParams.minPrice ? parseFloat(searchParams.minPrice) : undefined,
    maxPrice: searchParams.maxPrice ? parseFloat(searchParams.maxPrice) : undefined,
    minRating: searchParams.minRating ? parseFloat(searchParams.minRating) : undefined,
    inStock: searchParams.inStock === 'true',
    sortBy: searchParams.sortBy as any,
    page: searchParams.page ? parseInt(searchParams.page, 10) : 1,
  };
  
  // Get filtered products
  const result = getProducts(filters);
  const categories = getCategories();
  const brands = getBrands();
  const priceRange = getPriceRange();
  
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <h1 className="text-3xl font-bold mb-2">Products</h1>
        <p className="text-gray-600">
          {result.total} product{result.total !== 1 ? 's' : ''} found
        </p>
      </div>
      
      <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
        {/* Filters Sidebar */}
        <aside className="lg:col-span-1">
          <ProductFilters
            categories={categories}
            brands={brands}
            priceRange={priceRange}
            currentFilters={filters}
          />
        </aside>
        
        {/* Products */}
        <main className="lg:col-span-3">
          {/* Sort and View Options */}
          <div className="mb-6 flex items-center justify-between">
            <p className="text-sm text-gray-600">
              Showing {(result.page - 1) * result.perPage + 1}-
              {Math.min(result.page * result.perPage, result.total)} of {result.total}
            </p>
            <ProductSort currentSort={filters.sortBy} />
          </div>
          
          {/* Products Grid */}
          <Suspense fallback={<ProductGridSkeleton />}>
            <ProductGrid products={result.products} />
          </Suspense>
          
          {/* Pagination */}
          {result.totalPages > 1 && (
            <div className="mt-8">
              <Pagination
                currentPage={result.page}
                totalPages={result.totalPages}
              />
            </div>
          )}
        </main>
      </div>
    </div>
  );
}

function ProductGridSkeleton() {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="bg-gray-200 aspect-square rounded-lg mb-4" />
          <div className="h-4 bg-gray-200 rounded mb-2" />
          <div className="h-4 bg-gray-200 rounded w-2/3" />
        </div>
      ))}
    </div>
  );
}

// ✅ URL-based filtering
// ✅ Server-side rendering
// ✅ Suspense boundaries
// ✅ Pagination
// ✅ Loading states

Product Filters Component

components/ProductFilters.tsx
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { FilterParams } from '@/lib/types';

interface ProductFiltersProps {
  categories: string[];
  brands: string[];
  priceRange: { min: number; max: number };
  currentFilters: FilterParams;
}

export function ProductFilters({
  categories,
  brands,
  priceRange,
  currentFilters,
}: ProductFiltersProps) {
  const router = useRouter();
  const searchParams = useSearchParams();
  
  const updateFilters = (updates: Partial<FilterParams>) => {
    const params = new URLSearchParams(searchParams);
    
    // Update or remove parameters
    Object.entries(updates).forEach(([key, value]) => {
      if (value === undefined || value === null || value === '') {
        params.delete(key);
      } else if (Array.isArray(value)) {
        params.delete(key);
        value.forEach((v) => params.append(key, v.toString()));
      } else {
        params.set(key, value.toString());
      }
    });
    
    // Reset to page 1 when filters change
    if (Object.keys(updates).some((key) => key !== 'page')) {
      params.set('page', '1');
    }
    
    router.push(`/products?${params.toString()}`);
  };
  
  const clearFilters = () => {
    router.push('/products');
  };
  
  return (
    <div className="space-y-6">
      {/* Clear Filters */}
      <div className="flex items-center justify-between">
        <h3 className="font-semibold">Filters</h3>
        <button
          onClick={clearFilters}
          className="text-sm text-blue-600 hover:underline"
        >
          Clear all
        </button>
      </div>
      
      {/* Search */}
      <div>
        <label className="block text-sm font-medium mb-2">Search</label>
        <input
          type="text"
          placeholder="Search products..."
          defaultValue={currentFilters.search}
          onChange={(e) => updateFilters({ search: e.target.value })}
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
      </div>
      
      {/* Category */}
      <div>
        <label className="block text-sm font-medium mb-2">Category</label>
        <select
          value={currentFilters.category || ''}
          onChange={(e) => updateFilters({ category: e.target.value || undefined })}
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        >
          <option value="">All Categories</option>
          {categories.map((cat) => (
            <option key={cat} value={cat}>
              {cat}
            </option>
          ))}
        </select>
      </div>
      
      {/* Brand (Multi-select) */}
      <div>
        <label className="block text-sm font-medium mb-2">Brand</label>
        <div className="space-y-2 max-h-48 overflow-y-auto">
          {brands.map((brand) => (
            <label key={brand} className="flex items-center">
              <input
                type="checkbox"
                checked={currentFilters.brand?.includes(brand)}
                onChange={(e) => {
                  const current = currentFilters.brand || [];
                  const updated = e.target.checked
                    ? [...current, brand]
                    : current.filter((b) => b !== brand);
                  updateFilters({ brand: updated.length > 0 ? updated : undefined });
                }}
                className="mr-2"
              />
              <span className="text-sm">{brand}</span>
            </label>
          ))}
        </div>
      </div>
      
      {/* Price Range */}
      <div>
        <label className="block text-sm font-medium mb-2">Price Range</label>
        <div className="space-y-2">
          <input
            type="number"
            placeholder="Min"
            value={currentFilters.minPrice || ''}
            onChange={(e) =>
              updateFilters({ minPrice: e.target.value ? parseFloat(e.target.value) : undefined })
            }
            className="w-full px-3 py-2 border rounded-lg"
            min={priceRange.min}
            max={priceRange.max}
          />
          <input
            type="number"
            placeholder="Max"
            value={currentFilters.maxPrice || ''}
            onChange={(e) =>
              updateFilters({ maxPrice: e.target.value ? parseFloat(e.target.value) : undefined })
            }
            className="w-full px-3 py-2 border rounded-lg"
            min={priceRange.min}
            max={priceRange.max}
          />
        </div>
      </div>
      
      {/* Rating */}
      <div>
        <label className="block text-sm font-medium mb-2">Minimum Rating</label>
        <select
          value={currentFilters.minRating || ''}
          onChange={(e) =>
            updateFilters({ minRating: e.target.value ? parseFloat(e.target.value) : undefined })
          }
          className="w-full px-3 py-2 border rounded-lg"
        >
          <option value="">Any</option>
          <option value="4">4+ Stars</option>
          <option value="3">3+ Stars</option>
          <option value="2">2+ Stars</option>
        </select>
      </div>
      
      {/* In Stock */}
      <div>
        <label className="flex items-center">
          <input
            type="checkbox"
            checked={currentFilters.inStock || false}
            onChange={(e) => updateFilters({ inStock: e.target.checked || undefined })}
            className="mr-2"
          />
          <span className="text-sm">In Stock Only</span>
        </label>
      </div>
    </div>
  );
}

// ✅ URL-based state
// ✅ Multi-select filters
// ✅ Price range inputs
// ✅ Clear all filters
// ✅ Resets page on filter change

Shopping Cart Implementation

Cart Store with Zustand

lib/cart-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Product } from './types';

export interface CartItem {
  product: Product;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (product: Product, quantity?: number) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  getTotalItems: () => number;
  getTotalPrice: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      
      addItem: (product, quantity = 1) => {
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.product.id === product.id
          );
          
          if (existingItem) {
            // Update quantity if item exists
            return {
              items: state.items.map((item) =>
                item.product.id === product.id
                  ? { ...item, quantity: item.quantity + quantity }
                  : item
              ),
            };
          }
          
          // Add new item
          return {
            items: [...state.items, { product, quantity }],
          };
        });
      },
      
      removeItem: (productId) => {
        set((state) => ({
          items: state.items.filter((item) => item.product.id !== productId),
        }));
      },
      
      updateQuantity: (productId, quantity) => {
        if (quantity <= 0) {
          get().removeItem(productId);
          return;
        }
        
        set((state) => ({
          items: state.items.map((item) =>
            item.product.id === productId ? { ...item, quantity } : item
          ),
        }));
      },
      
      clearCart: () => {
        set({ items: [] });
      },
      
      getTotalItems: () => {
        return get().items.reduce((total, item) => total + item.quantity, 0);
      },
      
      getTotalPrice: () => {
        return get().items.reduce(
          (total, item) => total + item.product.price * item.quantity,
          0
        );
      },
    }),
    {
      name: 'shopping-cart', // localStorage key
    }
  )
);

// ✅ Zustand for state management
// ✅ Persist to localStorage
// ✅ Add, remove, update operations
// ✅ Total calculations
// ✅ Type-safe throughout

Cart Button Component

components/CartButton.tsx
'use client';

import Link from 'next/link';
import { useCartStore } from '@/lib/cart-store';

export function CartButton() {
  const totalItems = useCartStore((state) => state.getTotalItems());
  
  return (
    <Link
      href="/cart"
      className="relative inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
    >
      <svg
        className="w-5 h-5"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
        />
      </svg>
      <span>Cart</span>
      
      {/* Badge */}
      {totalItems > 0 && (
        <span className="absolute -top-2 -right-2 flex items-center justify-center w-6 h-6 text-xs font-bold bg-red-500 text-white rounded-full">
          {totalItems}
        </span>
      )}
    </Link>
  );
}

// ✅ Shows item count
// ✅ Badge for visual feedback
// ✅ Links to cart page
// ✅ Real-time updates

Add to Cart Button

components/AddToCartButton.tsx
'use client';

import { useState } from 'react';
import { useCartStore } from '@/lib/cart-store';
import { Product } from '@/lib/types';

interface AddToCartButtonProps {
  product: Product;
}

export function AddToCartButton({ product }: AddToCartButtonProps) {
  const [quantity, setQuantity] = useState(1);
  const [added, setAdded] = useState(false);
  const addItem = useCartStore((state) => state.addItem);
  
  const handleAddToCart = () => {
    addItem(product, quantity);
    setAdded(true);
    
    // Reset feedback after 2 seconds
    setTimeout(() => setAdded(false), 2000);
  };
  
  return (
    <div className="space-y-4">
      {/* Quantity Selector */}
      <div className="flex items-center gap-4">
        <label className="text-sm font-medium">Quantity:</label>
        <div className="flex items-center border rounded-lg">
          <button
            onClick={() => setQuantity(Math.max(1, quantity - 1))}
            className="px-3 py-2 hover:bg-gray-100"
            disabled={quantity <= 1}
          >
            -
          </button>
          <span className="px-4 py-2 border-x">{quantity}</span>
          <button
            onClick={() => setQuantity(Math.min(product.stockCount, quantity + 1))}
            className="px-3 py-2 hover:bg-gray-100"
            disabled={quantity >= product.stockCount}
          >
            +
          </button>
        </div>
      </div>
      
      {/* Add to Cart Button */}
      <button
        onClick={handleAddToCart}
        disabled={!product.inStock}
        className={`w-full px-6 py-3 rounded-lg font-semibold transition-colors ${
          added
            ? 'bg-green-600 text-white'
            : product.inStock
            ? 'bg-blue-600 text-white hover:bg-blue-700'
            : 'bg-gray-300 text-gray-500 cursor-not-allowed'
        }`}
      >
        {added ? '✓ Added to Cart' : product.inStock ? 'Add to Cart' : 'Out of Stock'}
      </button>
    </div>
  );
}

// ✅ Quantity selector
// ✅ Stock validation
// ✅ Visual feedback
// ✅ Disabled when out of stock

Shopping Cart Page

Cart Page Implementation

app/cart/page.tsx
'use client';

import { useCartStore } from '@/lib/cart-store';
import Image from 'next/image';
import Link from 'next/link';

export default function CartPage() {
  const { items, removeItem, updateQuantity, getTotalPrice } = useCartStore();
  const totalPrice = getTotalPrice();
  
  if (items.length === 0) {
    return (
      <div className="container mx-auto px-4 py-16 text-center">
        <h1 className="text-3xl font-bold mb-4">Your Cart is Empty</h1>
        <p className="text-gray-600 mb-8">
          Add some products to get started!
        </p>
        <Link
          href="/products"
          className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          Browse Products
        </Link>
      </div>
    );
  }
  
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Shopping Cart</h1>
      
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        {/* Cart Items */}
        <div className="lg:col-span-2 space-y-4">
          {items.map((item) => (
            <div
              key={item.product.id}
              className="flex gap-4 p-4 bg-white border rounded-lg"
            >
              {/* Product Image */}
              <div className="relative w-24 h-24 flex-shrink-0">
                <Image
                  src={item.product.image}
                  alt={item.product.name}
                  fill
                  className="object-cover rounded"
                />
              </div>
              
              {/* Product Info */}
              <div className="flex-1">
                <Link
                  href={`/products/${item.product.id}`}
                  className="font-semibold hover:text-blue-600"
                >
                  {item.product.name}
                </Link>
                <p className="text-sm text-gray-600 mt-1">
                  ${item.product.price.toFixed(2)}
                </p>
                
                {/* Quantity Controls */}
                <div className="flex items-center gap-2 mt-3">
                  <button
                    onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
                    className="px-2 py-1 border rounded hover:bg-gray-100"
                  >
                    -
                  </button>
                  <span className="px-3">{item.quantity}</span>
                  <button
                    onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
                    className="px-2 py-1 border rounded hover:bg-gray-100"
                    disabled={item.quantity >= item.product.stockCount}
                  >
                    +
                  </button>
                  
                  <button
                    onClick={() => removeItem(item.product.id)}
                    className="ml-auto text-sm text-red-600 hover:underline"
                  >
                    Remove
                  </button>
                </div>
              </div>
              
              {/* Item Total */}
              <div className="text-right">
                <p className="font-semibold">
                  ${(item.product.price * item.quantity).toFixed(2)}
                </p>
              </div>
            </div>
          ))}
        </div>
        
        {/* Order Summary */}
        <div className="lg:col-span-1">
          <div className="bg-gray-50 p-6 rounded-lg sticky top-8">
            <h2 className="text-xl font-bold mb-4">Order Summary</h2>
            
            <div className="space-y-2 mb-4">
              <div className="flex justify-between">
                <span>Subtotal</span>
                <span>${totalPrice.toFixed(2)}</span>
              </div>
              <div className="flex justify-between">
                <span>Shipping</span>
                <span>FREE</span>
              </div>
              <div className="flex justify-between">
                <span>Tax</span>
                <span>${(totalPrice * 0.1).toFixed(2)}</span>
              </div>
              <div className="border-t pt-2 flex justify-between font-bold text-lg">
                <span>Total</span>
                <span>${(totalPrice * 1.1).toFixed(2)}</span>
              </div>
            </div>
            
            <button className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-semibold mb-2">
              Proceed to Checkout
            </button>
            
            <Link
              href="/products"
              className="block text-center text-blue-600 hover:underline text-sm"
            >
              Continue Shopping
            </Link>
          </div>
        </div>
      </div>
    </div>
  );
}

// ✅ Empty cart state
// ✅ Quantity controls
// ✅ Remove items
// ✅ Order summary
// ✅ Sticky sidebar
// ✅ Responsive layout

Product Detail Page

Individual Product Page

app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
import Image from 'next/image';
import { getProductById, getProducts } from '@/lib/products';
import { AddToCartButton } from '@/components/AddToCartButton';
import { RelatedProducts } from '@/components/RelatedProducts';
import { Metadata } from 'next';

interface PageProps {
  params: { id: string };
}

// Generate static params
export async function generateStaticParams() {
  const { products } = getProducts({ perPage: 1000 });
  return products.map((product) => ({ id: product.id }));
}

// Generate metadata
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const product = getProductById(params.id);
  
  if (!product) {
    return {};
  }
  
  return {
    title: `${product.name} | Shop`,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [product.image],
    },
  };
}

export default function ProductDetailPage({ params }: PageProps) {
  const product = getProductById(params.id);
  
  if (!product) {
    notFound();
  }
  
  const discount = product.originalPrice
    ? Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)
    : 0;
  
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
        {/* Product Images */}
        <div>
          <div className="relative aspect-square mb-4 rounded-lg overflow-hidden">
            <Image
              src={product.image}
              alt={product.name}
              fill
              className="object-cover"
              priority
            />
            {discount > 0 && (
              <div className="absolute top-4 right-4 px-3 py-1 bg-red-500 text-white font-bold rounded">
                -{discount}%
              </div>
            )}
          </div>
          
          {/* Thumbnail Images */}
          {product.images.length > 1 && (
            <div className="grid grid-cols-4 gap-2">
              {product.images.map((img, i) => (
                <div key={i} className="relative aspect-square rounded overflow-hidden">
                  <Image src={img} alt={`${product.name} ${i + 1}`} fill className="object-cover" />
                </div>
              ))}
            </div>
          )}
        </div>
        
        {/* Product Info */}
        <div>
          <h1 className="text-3xl font-bold mb-2">{product.name}</h1>
          
          {/* Rating */}
          <div className="flex items-center gap-2 mb-4">
            <div className="flex items-center">
              {Array.from({ length: 5 }).map((_, i) => (
                <span key={i} className={i < Math.floor(product.rating) ? 'text-yellow-400' : 'text-gray-300'}>
                  ★
                </span>
              ))}
            </div>
            <span className="text-sm text-gray-600">
              {product.rating} ({product.reviewCount} reviews)
            </span>
          </div>
          
          {/* Price */}
          <div className="mb-6">
            <div className="flex items-center gap-3">
              <span className="text-3xl font-bold">${product.price.toFixed(2)}</span>
              {product.originalPrice && (
                <span className="text-xl text-gray-400 line-through">
                  ${product.originalPrice.toFixed(2)}
                </span>
              )}
            </div>
          </div>
          
          {/* Description */}
          <p className="text-gray-600 mb-6">{product.description}</p>
          
          {/* Stock Status */}
          <div className="mb-6">
            {product.inStock ? (
              <p className="text-green-600 font-semibold">
                In Stock ({product.stockCount} available)
              </p>
            ) : (
              <p className="text-red-600 font-semibold">Out of Stock</p>
            )}
          </div>
          
          {/* Add to Cart */}
          <AddToCartButton product={product} />
          
          {/* Specifications */}
          <div className="mt-8">
            <h2 className="text-xl font-bold mb-4">Specifications</h2>
            <dl className="space-y-2">
              {Object.entries(product.specifications).map(([key, value]) => (
                <div key={key} className="flex">
                  <dt className="w-1/3 text-gray-600">{key}:</dt>
                  <dd className="w-2/3 font-semibold">{value}</dd>
                </div>
              ))}
            </dl>
          </div>
          
          {/* Tags */}
          <div className="mt-6">
            <div className="flex flex-wrap gap-2">
              {product.tags.map((tag) => (
                <span key={tag} className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
                  {tag}
                </span>
              ))}
            </div>
          </div>
        </div>
      </div>
      
      {/* Related Products */}
      <div className="mt-16">
        <RelatedProducts currentProduct={product} />
      </div>
    </div>
  );
}

// ✅ Static generation
// ✅ Image gallery
// ✅ Price with discount
// ✅ Stock status
// ✅ Specifications
// ✅ Add to cart
// ✅ Related products
// ✅ SEO optimized

Key Takeaways

  • URL-based filtering - searchParams for shareable filters
  • Multi-criteria search - search, category, brand, price, rating
  • Zustand state management - cart with localStorage persistence
  • Static generation - pre-generate product pages
  • Responsive design - mobile-friendly layouts
  • Image optimization - Next.js Image component
  • Pagination - handle large product catalogs
  • Type safety - TypeScript throughout

What's Next?

You've built a complete e-commerce catalog! Next, we'll build Project 3: Dashboard with Authentication—a full authentication system with protected routes, user management, role-based access control, and an admin dashboard. You'll learn complete authentication flows and building secure applications!

💡 Project Enhancement Ideas

Extend your catalog: Add product reviews and ratings, implement wishlist functionality, add product comparison, create advanced search with autocomplete, implement real checkout with Stripe, add order tracking, create vendor/admin panels, or integrate with inventory management systems!

Test Your Understanding

Question 1 of 4

What's the best way to handle product filtering in Next.js?

Build a complete e-commerce catalog with filtering, search, and shopping cart in Next.js!

Previous
Performance Monitoring and Analytics
Next
Project 2: Dashboard with Authentication

Continue Building Real-World Projects

Join 2,000+ developers building production Next.js apps. Get the next project on authentication - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo