JB logo

Command Palette

Search for a command to run...

yOUTUBE
Components
Next

Re-usable Zustand Cart

A complete e-commerce shopping cart component with product listing, cart management.

Component zustand-cart-demo not found in registry.

Installation

pnpm dlx shadcn@latest add https://jb.desishub.com/r/zustand-cart.json

Usage

import { PesaPalCart } from "@/components/zustand-cart";
// In your page component
export default function ShopPage() {
  return (
    <div className="container mx-auto">
      <PesaPalCart />
    </div>
  );
}

Props

The PesaPalCart component accepts the following props:

PropTypeDescriptionDefault
productsProduct[](Optional) Array of products to display. If not provided, uses sample products.sampleProducts
showCategoryboolean(Optional) Whether to show category links on product cards.false

Types

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  image: string;
  categoryId?: string;
}
 
interface CartItem extends Product {
  quantity: number;
}
 
interface CartState {
  items: CartItem[];
  addToCart: (product: Product) => void;
  incrementQuantity: (productId: string) => void;
  decrementQuantity: (productId: string) => void;
  removeFromCart: (productId: string) => void;
  getCartTotalItems: () => number;
  getCartTotalPrice: () => number;
  clearCart: () => void;
}

Individual Components

ProductCard

The ProductCard component displays an individual product with image, details, and add to cart functionality.

import { ProductCard } from "@/components/product-card";
 
<ProductCard product={product} showCategory={true} />;

ProductListing

The ProductListing component displays a grid of product cards.

import { ProductListing } from "@/components/product-listing";
 
<ProductListing products={products} showCategory={true} />;

Cart

The Cart component displays the shopping cart with items, quantities, and checkout functionality.

import { Cart } from "@/components/cart";
 
<Cart />;

useCartStore Hook

The useCartStore hook provides access to the cart state and actions.

import { useCartStore } from "@/hooks/use-cart-store";
 
const { items, addToCart, removeFromCart, getCartTotalPrice } = useCartStore();

Examples

Basic Usage

import { PesaPalCart } from "@/components/zustand-cart";
 
export default function ShopPage() {
  return <PesaPalCart />;
}

Custom Products

import { PesaPalCart } from "@/components/zustand-cart";
 
const customProducts = [
  {
    id: "custom-1",
    name: "Custom Product",
    description: "This is a custom product",
    price: 99.99,
    image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e",
    categoryId: "custom",
  },
  // More products...
];
 
export default function ShopPage() {
  return <PesaPalCart products={customProducts} showCategory={true} />;
}

Using Individual Components

import { ProductListing, Cart } from "@/components/zustand-cart";
 
const products = [
  // Your products array
];
 
export default function ShopPage() {
  return (
    <div className="container mx-auto py-8">
      <h1 className="mb-8 text-3xl font-bold">Our Products</h1>
      <ProductListing products={products} />
      <Cart />
    </div>
  );
}

Customization

You can customize the appearance of the PesaPalCart component by modifying the CSS classes. The component uses Tailwind CSS for styling.

Styling Product Cards

Override the product card styles by targeting the classes in the ProductCard component.

Custom Checkout Integration

To integrate with PesaPal or another payment gateway, modify the checkout handler in the Cart component:

const handleCheckout = () => {
  // Replace with your payment integration
  // Example: redirectToPesaPalCheckout(items, totalPrice);
  alert(`Proceeding to checkout with total: $${totalPrice.toFixed(2)}`);
};

Examples

Component zustand-cart-demo not found in registry.

Now let me create the individual component files as referenced in the documentation:

1. use-cart-store.ts

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
 
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  image: string;
  categoryId?: string;
}
 
export interface CartItem extends Product {
  quantity: number;
}
 
interface CartState {
  items: CartItem[];
  addToCart: (product: Product) => void;
  incrementQuantity: (productId: string) => void;
  decrementQuantity: (productId: string) => void;
  removeFromCart: (productId: string) => void;
  getCartTotalItems: () => number;
  getCartTotalPrice: () => number;
  clearCart: () => void;
}
 
export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      addToCart: (product) => {
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.id === product.id
          );
          if (existingItem) {
            return {
              items: state.items.map((item) =>
                item.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          } else {
            return {
              items: [...state.items, { ...product, quantity: 1 }],
            };
          }
        });
      },
      incrementQuantity: (productId) => {
        set((state) => ({
          items: state.items.map((item) =>
            item.id === productId
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        }));
      },
      decrementQuantity: (productId) => {
        set((state) => ({
          items: state.items
            .map((item) =>
              item.id === productId
                ? { ...item, quantity: Math.max(0, item.quantity - 1) }
                : item
            )
            .filter((item) => item.quantity > 0),
        }));
      },
      removeFromCart: (productId) => {
        set((state) => ({
          items: state.items.filter((item) => item.id !== productId),
        }));
      },
      getCartTotalItems: () => {
        return get().items.reduce((total, item) => total + item.quantity, 0);
      },
      getCartTotalPrice: () => {
        return get().items.reduce(
          (total, item) => total + item.price * item.quantity,
          0
        );
      },
      clearCart: () => {
        set({ items: [] });
      },
    }),
    {
      name: "ecommerce-cart-storage",
      storage: createJSONStorage(() => localStorage),
    }
  )
);

2. product-card.tsx

"use client";
 
import Link from "next/link";
import { ShoppingCart, Heart } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Product } from "./use-cart-store";
import { useCartStore } from "./use-cart-store";
 
interface ProductCardProps {
  product: Product;
  showCategory?: boolean;
}
 
export function ProductCard({
  product,
  showCategory = false,
}: ProductCardProps) {
  const addToCart = useCartStore((state) => state.addToCart);
 
  return (
    <div className="group overflow-hidden rounded-lg bg-white shadow-md transition-all duration-300 hover:shadow-xl">
      <Link href={`/products/${product.id}`}>
        <div className="relative h-48 overflow-hidden">
          <img
            src={product.image}
            alt={product.name}
            className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
            onError={(e) => {
              const target = e.target as HTMLImageElement;
              target.src = "https://via.placeholder.com/400x300?text=Product";
            }}
          />
          <div className="absolute inset-0 bg-black bg-opacity-0 transition-all duration-300 group-hover:bg-opacity-20" />
 
          {/* Quick actions overlay */}
          <div className="absolute right-4 top-4 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
            <button className="rounded-full bg-white p-2 shadow-md transition-colors hover:bg-gray-50">
              <Heart className="h-4 w-4 text-gray-600" />
            </button>
          </div>
        </div>
      </Link>
 
      <div className="p-4">
        <Link href={`/products/${product.id}`}>
          <h3 className="mb-2 line-clamp-2 text-lg font-semibold text-gray-900 transition-colors group-hover:text-blue-600">
            {product.name}
          </h3>
        </Link>
 
        <p className="mb-3 line-clamp-2 text-sm text-gray-600">
          {product.description}
        </p>
 
        <div className="flex items-center justify-between">
          <span className="text-xl font-bold text-green-600">
            ${product.price.toFixed(2)}
          </span>
 
          <Button
            size="sm"
            className="flex items-center gap-2"
            onClick={(e) => {
              e.preventDefault();
              addToCart(product);
            }}
          >
            <ShoppingCart className="h-4 w-4" />
            Add to Cart
          </Button>
        </div>
 
        {showCategory && product.categoryId && (
          <div className="mt-3 border-t border-gray-100 pt-3">
            <Link
              href={`/categories/${product.categoryId}`}
              className="text-xs text-blue-600 transition-colors hover:text-blue-800"
            >
              View Category →
            </Link>
          </div>
        )}
      </div>
    </div>
  );
}

3. product-listing.tsx

import { Product } from "./use-cart-store";
import { ProductCard } from "./product-card";
 
interface ProductListingProps {
  products: Product[];
  showCategory?: boolean;
}
 
export function ProductListing({
  products,
  showCategory = false,
}: ProductListingProps) {
  return (
    <div className="mb-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          showCategory={showCategory}
        />
      ))}
    </div>
  );
}

4. cart.tsx

"use client";
 
import { useState } from "react";
import { ShoppingCart, X, Plus, Minus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useCartStore } from "./use-cart-store";
 
export function Cart() {
  const {
    items,
    incrementQuantity,
    decrementQuantity,
    removeFromCart,
    getCartTotalPrice,
    clearCart,
  } = useCartStore();
 
  const [isOpen, setIsOpen] = useState(false);
  const totalItems = useCartStore((state) => state.getCartTotalItems());
  const totalPrice = getCartTotalPrice();
 
  const handleCheckout = () => {
    alert(`Proceeding to checkout with total: $${totalPrice.toFixed(2)}`);
    // Here you would integrate with PesaPal or other payment gateway
  };
 
  return (
    <div className="fixed bottom-4 right-4 z-50">
      {/* Cart Toggle Button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="relative rounded-full bg-blue-600 p-4 text-white shadow-lg transition-colors hover:bg-blue-700"
      >
        <ShoppingCart className="h-6 w-6" />
        {totalItems > 0 && (
          <span className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
            {totalItems}
          </span>
        )}
      </button>
 
      {/* Cart Panel */}
      {isOpen && (
        <div className="absolute bottom-16 right-0 flex max-h-96 w-80 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-xl">
          <div className="flex items-center justify-between border-b border-gray-200 p-4">
            <h3 className="text-lg font-semibold">
              Your Cart ({totalItems} items)
            </h3>
            <button
              onClick={() => setIsOpen(false)}
              className="text-gray-500 hover:text-gray-700"
            >
              <X className="h-5 w-5" />
            </button>
          </div>
 
          <div className="flex-1 overflow-y-auto p-4">
            {items.length === 0 ? (
              <p className="py-8 text-center text-gray-500">
                Your cart is empty
              </p>
            ) : (
              <div className="space-y-4">
                {items.map((item) => (
                  <div
                    key={item.id}
                    className="flex items-center justify-between border-b pb-3"
                  >
                    <div className="flex items-center space-x-3">
                      <img
                        src={item.image}
                        alt={item.name}
                        className="h-12 w-12 rounded object-cover"
                        onError={(e) => {
                          const target = e.target as HTMLImageElement;
                          target.src =
                            "https://via.placeholder.com/100x100?text=Product";
                        }}
                      />
                      <div>
                        <h4 className="line-clamp-1 text-sm font-medium">
                          {item.name}
                        </h4>
                        <p className="font-semibold text-green-600">
                          ${item.price.toFixed(2)}
                        </p>
                      </div>
                    </div>
 
                    <div className="flex items-center space-x-2">
                      <button
                        onClick={() => decrementQuantity(item.id)}
                        className="rounded-full bg-gray-100 p-1 hover:bg-gray-200"
                      >
                        <Minus className="h-3 w-3" />
                      </button>
 
                      <span className="text-sm font-medium">
                        {item.quantity}
                      </span>
 
                      <button
                        onClick={() => incrementQuantity(item.id)}
                        className="rounded-full bg-gray-100 p-1 hover:bg-gray-200"
                      >
                        <Plus className="h-3 w-3" />
                      </button>
 
                      <button
                        onClick={() => removeFromCart(item.id)}
                        className="ml-2 p-1 text-red-500 hover:text-red-700"
                      >
                        <X className="h-4 w-4" />
                      </button>
                    </div>
                  </div>
                ))}
              </div>
            )}
          </div>
 
          {items.length > 0 && (
            <div className="border-t border-gray-200 p-4">
              <div className="mb-4 flex items-center justify-between">
                <span className="font-semibold">Total:</span>
                <span className="text-xl font-bold text-green-600">
                  ${totalPrice.toFixed(2)}
                </span>
              </div>
 
              <div className="flex space-x-2">
                <Button
                  variant="outline"
                  className="flex-1"
                  onClick={clearCart}
                >
                  Clear Cart
                </Button>
                <Button
                  className="flex-1 bg-green-600 hover:bg-green-700"
                  onClick={handleCheckout}
                >
                  Pay Now
                </Button>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

5. zustand-cart.tsx (Main Component)

import { ProductListing } from "./product-listing";
import { Cart } from "./cart";
import { Product } from "./use-cart-store";
 
// Sample product data with Unsplash images
const sampleProducts: Product[] = [
  {
    id: "1",
    name: "Premium Headphones",
    description:
      "Noise-cancelling wireless headphones with exceptional sound quality and comfort.",
    price: 199.99,
    image:
      "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
    categoryId: "electronics",
  },
  {
    id: "2",
    name: "Smart Watch Series 5",
    description:
      "Advanced smartwatch with health monitoring, GPS, and long battery life.",
    price: 249.99,
    image:
      "https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
    categoryId: "electronics",
  },
  {
    id: "3",
    name: "Designer Backpack",
    description:
      "Durable and stylish backpack with multiple compartments and laptop sleeve.",
    price: 89.99,
    image:
      "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
    categoryId: "fashion",
  },
];
 
interface PesaPalCartProps {
  products?: Product[];
  showCategory?: boolean;
}
 
export function PesaPalCart({
  products = sampleProducts,
  showCategory = false,
}: PesaPalCartProps) {
  return (
    <div className="container mx-auto px-4 py-8">
      <h2 className="mb-8 text-center text-3xl font-bold">Featured Products</h2>
 
      <ProductListing products={products} showCategory={showCategory} />
 
      <Cart />
    </div>
  );
}

6. index.ts (Barrel File)

export { PesaPalCart } from "./zustand-cart";
export { ProductCard } from "./product-card";
export { ProductListing } from "./product-listing";
export { Cart } from "./cart";
export { useCartStore } from "./use-cart-store";
export type { Product, CartItem } from "./use-cart-store";

This implementation provides a complete, reusable e-commerce cart component with Zustand state management, individual sub-components, and comprehensive documentation.