Back to roadmaps nextjs Course

Project: Interactive E-commerce Catalog

In this project, we will build a product catalog catalog page. The page fetches products on the server, filters items dynamically based on URL search query parameters, and updates a shopping cart using Server Actions.


1. Project Specifications

  • Search Parameters: Filters products by Category and Name using Next.js searchParams.
  • Server-side Filtering: Database filtration is executed directly on the server to keep page bundles lightweight.
  • Server Actions: Users can add products to a shopping cart session securely.

2. Implementing the Catalog Page

Create the component at src/app/catalog/page.tsx and paste the following implementation:

// src/app/catalog/page.tsx
import { revalidatePath } from "next/cache";

interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

const mockProducts: Product[] = [
  { id: 1, name: "Mechanical Keyboard", category: "hardware", price: 89 },
  { id: 2, name: "Wired Gaming Mouse", category: "hardware", price: 49 },
  { id: 3, name: "Sleek Coffee Mug", category: "kitchen", price: 15 },
  { id: 4, name: "Cast Iron Skillet", category: "kitchen", price: 35 }
];

// Server Action helper to add items to cart
async function addToCartAction(formData: FormData) {
  "use server";
  const productId = formData.get("productId");
  
  // Save to user session cookie or database store
  console.log(`[ACTION] Item successfully added to cart: ${productId}`);

  // Revalidate to show updated items count
  revalidatePath("/catalog");
}

interface CatalogProps {
  searchParams: Promise<{ query?: string; category?: string }>;
}

export default async function CatalogPage({ searchParams }: CatalogProps) {
  const resolvedParams = await searchParams;
  const query = resolvedParams.query || "";
  const category = resolvedParams.category || "";

  // Filter products based on URL parameters
  const filteredProducts = mockProducts.filter((product) => {
    const matchesQuery = product.name.toLowerCase().includes(query.toLowerCase());
    const matchesCategory = category ? product.category === category : true;
    return matchesQuery && matchesCategory;
  });

  return (
    <div className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold">Product Catalog</h1>

      {/* Filter Options */}
      <form method="GET" action="/catalog" className="flex gap-4 mt-6">
        <input
          name="query"
          defaultValue={query}
          placeholder="Search items..."
          className="border p-2 rounded flex-1"
        />
        <select name="category" defaultValue={category} className="border p-2 rounded">
          <option value="">All Categories</option>
          <option value="hardware">Hardware</option>
          <option value="kitchen">Kitchen</option>
        </select>
        <button type="submit" className="bg-blue-600 text-white px-4 rounded">
          Filter
        </button>
      </form>

      {/* Catalog Grid */}
      <div className="grid grid-cols-2 gap-6 mt-8">
        {filteredProducts.map((product) => (
          <div key={product.id} className="border p-6 rounded shadow-sm">
            <h3 className="font-bold text-lg">{product.name}</h3>
            <p className="text-gray-500 text-sm">Category: {product.category}</p>
            <p className="text-xl font-semibold mt-2">${product.price}</p>
            
            {/* Add to cart button utilizing a Server Action */}
            <form action={addToCartAction} className="mt-4">
              <input type="hidden" name="productId" value={product.id} />
              <button
                type="submit"
                className="w-full bg-green-600 text-white py-2 rounded hover:bg-green-700"
              >
                Add to Cart
              </button>
            </form>
          </div>
        ))}
      </div>
    </div>
  );
}

Since the search form uses a standard GET submission, clicking "Filter" updates the URL browser parameters automatically (for example, /catalog?query=keyboard&category=hardware), which triggers an on-demand server render with filtered results.

Published on Last updated: