> ## Documentation Index
> Fetch the complete documentation index at: https://twigz.arqqin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer Products

> Browse store products with personalized recommendations and social features

## Overview

The Customer Products API provides product browsing functionality with personalized features including liked products, customer-specific pricing, and social interactions. This system enhances the shopping experience with recommendations and social proof.

**Location:** `convex/customers/products.ts`

## Get Products

Retrieve products for a store with optional category filtering and customer personalization.

<ParamField path="customerId" type="Id<'customers'>">
  Customer ID for personalized results (shows liked status, recommendations)
</ParamField>

<ParamField path="storeId" type="Id<'stores'>" required>
  Store ID to get products from
</ParamField>

<ParamField path="categoryId" type="Id<'categories'>">
  Optional category filter to show only products in specific category
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const products = await convex.query(api.customers.products.getProducts, {
    customerId: "c123456789",
    storeId: "j123456789",
    categoryId: "k123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  // Get all products for a store
  const allProducts = await convex.query("customers.products.getProducts", {
    storeId: "j123456789"
  });

  // Get products with customer personalization
  const personalizedProducts = await convex.query("customers.products.getProducts", {
    customerId: "c123456789",
    storeId: "j123456789"
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"{CONVEX_URL}/api/query",
      json={
          "path": "customers.products.getProducts",
          "args": {
              "customerId": "c123456789",
              "storeId": "j123456789",
              "categoryId": "k123456789"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

  products = response.json()
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "_id": "p123456789",
      "name": "Pizza Margherita",
      "description": "Classic Italian pizza with fresh mozzarella, tomatoes, and basil",
      "price": 15.99,
      "stock": 25,
      "stockAlert": 10,
      "prepTime": 20,
      "prepTimeUnit": "minutes",
      "isActive": true,
      "primaryImageUrl": "https://storage.convex.dev/pizza-margherita.jpg",
      "category": {
        "id": "k123456789",
        "name": "Italian Cuisine"
      },
      "isCustomerLikesProduct": true,
      "_creationTime": 1640995200000
    },
    {
      "_id": "p987654321",
      "name": "Garlic Bread",
      "description": "Freshly baked garlic bread with herbs and parmesan",
      "price": 6.99,
      "stock": 50,
      "prepTime": 10,
      "prepTimeUnit": "minutes",
      "isActive": true,
      "primaryImageUrl": "https://storage.convex.dev/garlic-bread.jpg",
      "category": {
        "id": "k123456789",
        "name": "Italian Cuisine"
      },
      "isCustomerLikesProduct": false,
      "_creationTime": 1640994000000
    }
  ]
  ```
</ResponseExample>

## Get Liked Products

Retrieve products that a customer has liked.

<ParamField path="customerId" type="Id<'customers'>">
  Customer ID (defaults to authenticated user)
</ParamField>

<ParamField path="categoryId" type="Id<'categories'>">
  Optional category filter
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const likedProducts = await convex.query(api.customers.products.getLikedProducts, {
    customerId: "c123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  const likedProducts = await convex.query("customers.products.getLikedProducts", {
    customerId: "c123456789",
    categoryId: "k123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "_id": "p123456789",
      "name": "Pizza Margherita",
      "price": 15.99,
      "primaryImageUrl": "https://storage.convex.dev/pizza-margherita.jpg",
      "category": { "id": "k123456789", "name": "Italian Cuisine" },
      "globalCategoryId": { "id": "g111222333", "name": "Food" },
      "store": { "id": "j123456789", "name": "Mario's Pizza Palace", "logoUrl": "https://storage.convex.dev/mario-logo.jpg" },
      "isCustomerLikesProduct": true
    }
  ]
  ```
</ResponseExample>

## Product Browsing Components

Complete product browsing interface with social features:

<CodeGroup>
  ```typescript React Product Browser theme={null}
  import { useState } from 'react';
  import { useQuery, useMutation } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface ProductBrowserProps {
    storeId: string;
    customerId?: string;
    onAddToCart: (productId: string, quantity: number) => void;
  }

  function ProductBrowser({ storeId, customerId, onAddToCart }: ProductBrowserProps) {
    const [selectedCategory, setSelectedCategory] = useState<string>('');
    const [quantities, setQuantities] = useState<Record<string, number>>({});

    const products = useQuery(api.customers.products.getProducts, {
      customerId,
      storeId,
      categoryId: selectedCategory || undefined
    });

    const storeCategories = useQuery(api.shared.categories.getAvailableCategoriesForStore, {
      storeId
    });

    const likeProduct = useMutation(api.customers.feed.likeProduct);
    const unlikeProduct = useMutation(api.customers.feed.unlikeProduct);

    const handleLikeToggle = async (productId: string, isLiked: boolean) => {
      if (!customerId) return;

      try {
        if (isLiked) {
          await unlikeProduct({ productId, customerId });
        } else {
          await likeProduct({ productId, customerId });
        }
      } catch (error) {
        console.error('Failed to toggle like:', error);
      }
    };

    const handleQuantityChange = (productId: string, quantity: number) => {
      setQuantities(prev => ({ ...prev, [productId]: quantity }));
    };

    const handleAddToCart = (productId: string) => {
      const quantity = quantities[productId] || 1;
      onAddToCart(productId, quantity);
      setQuantities(prev => ({ ...prev, [productId]: 1 })); // Reset to 1
    };

    return (
      <div className="space-y-6">
        {/* Category Filter */}
        <div className="bg-white p-4 rounded-lg shadow">
          <div className="flex flex-wrap gap-2">
            <button
              onClick={() => setSelectedCategory('')}
              className={`px-4 py-2 rounded-full text-sm font-medium ${
                selectedCategory === '' 
                  ? 'bg-blue-600 text-white' 
                  : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
              }`}
            >
              All Products
            </button>
            
            {storeCategories?.map((category) => (
              <button
                key={category._id}
                onClick={() => setSelectedCategory(category._id)}
                className={`px-4 py-2 rounded-full text-sm font-medium ${
                  selectedCategory === category._id 
                    ? 'bg-blue-600 text-white' 
                    : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
                }`}
              >
                {category.name}
              </button>
            ))}
          </div>
        </div>

        {/* Products Grid */}
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products?.map((product) => (
            <div key={product._id} className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow">
              {/* Product Image */}
              <div className="relative h-48 bg-gray-200 rounded-t-lg overflow-hidden">
                <img
                  src={product.primaryImageUrl}
                  alt={product.name}
                  className="w-full h-full object-cover"
                />
                
                {/* Like Button */}
                {customerId && (
                  <button
                    onClick={() => handleLikeToggle(product._id, product.isCustomerLikesProduct)}
                    className={`absolute top-2 right-2 p-2 rounded-full ${
                      product.isCustomerLikesProduct 
                        ? 'bg-red-500 text-white' 
                        : 'bg-white text-gray-600 hover:bg-gray-50'
                    }`}
                  >
                    <svg className="w-5 h-5" fill={product.isCustomerLikesProduct ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
                    </svg>
                  </button>
                )}

                {/* Stock Badge */}
                {product.stock <= product.stockAlert && (
                  <div className="absolute top-2 left-2 px-2 py-1 bg-orange-500 text-white text-xs font-medium rounded">
                    Low Stock
                  </div>
                )}
              </div>

              <div className="p-4">
                {/* Product Info */}
                <div className="mb-3">
                  <h3 className="font-semibold text-gray-900 mb-1">{product.name}</h3>
                  <p className="text-sm text-gray-600 line-clamp-2">{product.description}</p>
                  <p className="text-xs text-gray-500 mt-1">{product.category.name}</p>
                </div>

                {/* Price and Prep Time */}
                <div className="flex items-center justify-between mb-3">
                  <span className="text-lg font-bold text-gray-900">${product.price.toFixed(2)}</span>
                  <span className="text-sm text-gray-600">
                    ⏱️ {product.prepTime} {product.prepTimeUnit}
                  </span>
                </div>

                {/* Social Stats */}
                <div className="flex items-center space-x-4 text-xs text-gray-500 mb-3">
                  <span>❤️ {product.socialStats?.likesCount || 0}</span>
                  <span>📤 {product.socialStats?.sharesCount || 0}</span>
                  <span>🛒 {product.socialStats?.ordersCount || 0} orders</span>
                </div>

                {/* Add to Cart */}
                <div className="flex items-center space-x-2">
                  <div className="flex items-center border rounded">
                    <button
                      onClick={() => handleQuantityChange(product._id, Math.max(1, (quantities[product._id] || 1) - 1))}
                      className="px-2 py-1 hover:bg-gray-100"
                    >
                      -
                    </button>
                    <span className="px-3 py-1 min-w-[3rem] text-center">
                      {quantities[product._id] || 1}
                    </span>
                    <button
                      onClick={() => handleQuantityChange(product._id, (quantities[product._id] || 1) + 1)}
                      className="px-2 py-1 hover:bg-gray-100"
                    >
                      +
                    </button>
                  </div>
                  
                  <button
                    onClick={() => handleAddToCart(product._id)}
                    disabled={!product.isActive || product.stock === 0}
                    className="flex-1 bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
                  >
                    {product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
                  </button>
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* Empty State */}
        {products && products.length === 0 && (
          <div className="text-center py-12">
            <div className="w-24 h-24 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
              <svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
              </svg>
            </div>
            <h3 className="text-lg font-medium text-gray-900 mb-2">No products found</h3>
            <p className="text-gray-600">
              {selectedCategory 
                ? 'No products in this category' 
                : 'This store hasn\'t added any products yet'
              }
            </p>
          </div>
        )}
      </div>
    );
  }

  export default ProductBrowser;
  ```

  ```typescript Product Card Component theme={null}
  import { useState } from 'react';
  import { useMutation } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface ProductCardProps {
    product: any;
    customerId?: string;
    onAddToCart: (productId: string, quantity: number) => void;
    onViewDetails: (productId: string) => void;
  }

  function ProductCard({ product, customerId, onAddToCart, onViewDetails }: ProductCardProps) {
    const [quantity, setQuantity] = useState(1);
    const [isLiking, setIsLiking] = useState(false);

    const likeProduct = useMutation(api.customers.feed.likeProduct);
    const unlikeProduct = useMutation(api.customers.feed.unlikeProduct);

    const handleLikeToggle = async () => {
      if (!customerId || isLiking) return;

      setIsLiking(true);
      try {
        if (product.isCustomerLikesProduct) {
          await unlikeProduct({ productId: product._id, customerId });
        } else {
          await likeProduct({ productId: product._id, customerId });
        }
      } catch (error) {
        console.error('Failed to toggle like:', error);
      } finally {
        setIsLiking(false);
      }
    };

    const isOutOfStock = product.stock === 0;
    const isLowStock = product.stock <= product.stockAlert && product.stock > 0;

    return (
      <div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 overflow-hidden">
        {/* Product Image */}
        <div className="relative h-48 bg-gray-100">
          <img
            src={product.primaryImageUrl}
            alt={product.name}
            className="w-full h-full object-cover"
            loading="lazy"
          />
          
          {/* Badges */}
          <div className="absolute top-3 left-3 flex flex-col space-y-1">
            {isOutOfStock && (
              <span className="px-2 py-1 bg-red-500 text-white text-xs font-medium rounded">
                Out of Stock
              </span>
            )}
            {isLowStock && (
              <span className="px-2 py-1 bg-orange-500 text-white text-xs font-medium rounded">
                Low Stock
              </span>
            )}
          </div>

          {/* Like Button */}
          {customerId && (
            <button
              onClick={handleLikeToggle}
              disabled={isLiking}
              className={`absolute top-3 right-3 p-2 rounded-full shadow-sm transition-all ${
                product.isCustomerLikesProduct 
                  ? 'bg-red-500 text-white' 
                  : 'bg-white text-gray-600 hover:bg-gray-50'
              } ${isLiking ? 'opacity-50' : ''}`}
            >
              <svg className="w-4 h-4" fill={product.isCustomerLikesProduct ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
              </svg>
            </button>
          )}
        </div>

        <div className="p-4">
          {/* Product Info */}
          <div className="mb-3">
            <h3 className="font-semibold text-gray-900 mb-1 line-clamp-1">{product.name}</h3>
            <p className="text-sm text-gray-600 line-clamp-2 mb-2">{product.description}</p>
            <span className="inline-block px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded">
              {product.category.name}
            </span>
          </div>

          {/* Price and Timing */}
          <div className="flex items-center justify-between mb-3">
            <span className="text-xl font-bold text-gray-900">${product.price.toFixed(2)}</span>
            <span className="text-sm text-gray-600 bg-gray-50 px-2 py-1 rounded">
              ⏱️ {product.prepTime} {product.prepTimeUnit}
            </span>
          </div>

          {/* Social Proof */}
          <div className="flex items-center justify-between text-xs text-gray-500 mb-4">
            <div className="flex items-center space-x-3">
              <span className="flex items-center">
                ❤️ {product.socialStats?.likesCount || 0}
              </span>
              <span className="flex items-center">
                🛒 {product.socialStats?.ordersCount || 0}
              </span>
            </div>
            <button
              onClick={() => onViewDetails(product._id)}
              className="text-blue-600 hover:text-blue-800"
            >
              View Details →
            </button>
          </div>

          {/* Add to Cart Section */}
          <div className="flex items-center space-x-2">
            <div className="flex items-center border rounded-lg">
              <button
                onClick={() => setQuantity(Math.max(1, quantity - 1))}
                className="px-3 py-2 hover:bg-gray-50 text-gray-600"
              >
                -
              </button>
              <span className="px-4 py-2 min-w-[3rem] text-center font-medium">
                {quantity}
              </span>
              <button
                onClick={() => setQuantity(quantity + 1)}
                className="px-3 py-2 hover:bg-gray-50 text-gray-600"
              >
                +
              </button>
            </div>
            
            <button
              onClick={() => handleAddToCart(product._id)}
              disabled={isOutOfStock}
              className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors"
            >
              {isOutOfStock ? 'Out of Stock' : 'Add to Cart'}
            </button>
          </div>
        </div>
      </div>
    );
  }

  export default ProductCard;
  ```
</CodeGroup>

## Liked Products Page

Complete liked products management:

<CodeGroup>
  ```typescript React Liked Products theme={null}
  import { useQuery, useMutation } from 'convex/react';
  import { api } from './convex/_generated/api';

  function LikedProductsPage({ customerId }: { customerId: string }) {
    const [cursor, setCursor] = useState<string | null>(null);
    const [allLikedProducts, setAllLikedProducts] = useState<any[]>([]);

    const likedProducts = useQuery(api.customers.products.getLikedProducts, {
      customerId,
      paginationOpts: { numItems: 20, cursor }
    });

    const unlikeProduct = useMutation(api.customers.feed.unlikeProduct);

    useEffect(() => {
      if (likedProducts) {
        if (cursor === null) {
          setAllLikedProducts(likedProducts.page);
        } else {
          setAllLikedProducts(prev => [...prev, ...likedProducts.page]);
        }
      }
    }, [likedProducts, cursor]);

    const handleUnlike = async (productId: string) => {
      try {
        await unlikeProduct({ productId, customerId });
        setAllLikedProducts(prev => prev.filter(p => p._id !== productId));
      } catch (error) {
        console.error('Failed to unlike product:', error);
      }
    };

    const handleLoadMore = () => {
      if (likedProducts && !likedProducts.isDone && likedProducts.continueCursor) {
        setCursor(likedProducts.continueCursor);
      }
    };

    return (
      <div className="max-w-6xl mx-auto space-y-6">
        <div className="text-center">
          <h1 className="text-3xl font-bold text-gray-900">Your Liked Products</h1>
          <p className="text-gray-600 mt-2">
            {allLikedProducts.length} products you've liked
          </p>
        </div>

        {allLikedProducts.length > 0 ? (
          <>
            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
              {allLikedProducts.map((product) => (
                <div key={product._id} className="bg-white rounded-lg shadow">
                  <div className="relative h-40 bg-gray-200 rounded-t-lg overflow-hidden">
                    <img
                      src={product.primaryImageUrl}
                      alt={product.name}
                      className="w-full h-full object-cover"
                    />
                    <button
                      onClick={() => handleUnlike(product._id)}
                      className="absolute top-2 right-2 p-2 bg-red-500 text-white rounded-full hover:bg-red-600"
                    >
                      <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
                        <path d="M6 18L18 6M6 6l12 12" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"/>
                      </svg>
                    </button>
                  </div>

                  <div className="p-4">
                    <h3 className="font-semibold text-gray-900 mb-1">{product.name}</h3>
                    <p className="text-sm text-gray-600 mb-2">{product.store.name}</p>
                    
                    <div className="flex items-center justify-between">
                      <span className="text-lg font-bold text-gray-900">
                        ${product.price.toFixed(2)}
                      </span>
                      <span className="text-xs text-gray-500">
                        Liked {new Date(product.likedAt).toLocaleDateString()}
                      </span>
                    </div>

                    <button
                      onClick={() => {/* Navigate to store */}}
                      className="w-full mt-3 bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
                    >
                      Visit Store
                    </button>
                  </div>
                </div>
              ))}
            </div>

            {likedProducts && !likedProducts.isDone && (
              <div className="text-center">
                <button
                  onClick={handleLoadMore}
                  className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
                >
                  Load More
                </button>
              </div>
            )}
          </>
        ) : (
          <div className="text-center py-12">
            <div className="w-24 h-24 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
              <svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
              </svg>
            </div>
            <h3 className="text-lg font-medium text-gray-900 mb-2">No liked products yet</h3>
            <p className="text-gray-600 mb-4">
              Start exploring stores and like products you're interested in!
            </p>
            <button className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
              Explore Stores
            </button>
          </div>
        )}
      </div>
    );
  }

  export default LikedProductsPage;
  ```
</CodeGroup>

## Product Personalization

The system provides personalized product experiences:

<CardGroup cols={2}>
  <Card title="Like Status" icon="heart">
    Shows whether the customer has liked each product for quick recognition
  </Card>

  <Card title="Social Proof" icon="users">
    Displays likes, shares, and order counts to help customers make decisions
  </Card>

  <Card title="Recommendations" icon="sparkles">
    Future: AI-powered recommendations based on liked products and order history
  </Card>

  <Card title="Wishlist" icon="bookmark">
    Liked products serve as a wishlist for future purchases
  </Card>
</CardGroup>

## Product Interaction Features

<AccordionGroup>
  <Accordion title="Product Liking">
    Customers can like/unlike products for wishlist and social features:

    ```typescript theme={null}
    // Like a product
    await convex.mutation(api.customers.feed.likeProduct, {
      productId: "p123456789",
      customerId: "c123456789"
    });

    // Unlike a product
    await convex.mutation(api.customers.feed.unlikeProduct, {
      productId: "p123456789", 
      customerId: "c123456789"
    });
    ```
  </Accordion>

  <Accordion title="Product Sharing">
    Share products on social platforms:

    ```typescript theme={null}
    await convex.mutation(api.customers.feed.share, {
      productId: "p123456789",
      customerId: "c123456789",
      platform: "whatsapp",
      message: "Check out this amazing pizza!"
    });
    ```
  </Accordion>

  <Accordion title="Stock Monitoring">
    Real-time stock level monitoring:

    ```typescript theme={null}
    // Products automatically show:
    // - Out of stock badge when stock = 0
    // - Low stock warning when stock <= stockAlert
    // - Available quantity for cart additions
    ```
  </Accordion>
</AccordionGroup>

## Error Handling

<AccordionGroup>
  <Accordion title="Store not found">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "Store not found",
      "storeId": "j123456789"
    }
    ```
  </Accordion>

  <Accordion title="No products available">
    **Status Code**: `200` (Success with empty array)

    ```json theme={null}
    []
    ```
  </Accordion>

  <Accordion title="Category not found">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "Category not found",
      "categoryId": "k123456789"
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  Product data includes real-time stock levels, social interaction counts, and personalized information like like status for enhanced shopping experience.
</Note>

<Tip>
  Use the social stats (likes, shares, orders) to highlight popular products and help customers discover trending items in each store.
</Tip>
