> ## 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.

# Store Exploration

> Discover stores with pagination, location-based search, and filtering

## Overview

The Store Exploration API provides comprehensive store discovery functionality including paginated browsing, location-based search, category filtering, and text search. This system powers the main store discovery experience for customers.

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

## Get Stores with Pagination

Retrieve stores with pagination and optional category filtering, perfect for infinite scroll implementations.

<ParamField path="paginationOpts" type="PaginationOptions" required>
  Pagination configuration with `numItems` and `cursor`
</ParamField>

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

<CodeGroup>
  ```typescript TypeScript theme={null}
  const stores = await convex.query(api.customers.explore.getStoresWithPagination, {
    paginationOpts: { numItems: 20, cursor: null },
    categoryId: "k123456789" // Food & Restaurants
  });
  ```

  ```javascript JavaScript theme={null}
  const stores = await convex.query("customers.explore.getStoresWithPagination", {
    paginationOpts: { numItems: 15, cursor: null }
  });
  ```

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

  response = requests.post(
      f"{CONVEX_URL}/api/query",
      json={
          "path": "customers.explore.getStoresWithPagination",
          "args": {
              "paginationOpts": {"numItems": 20, "cursor": None},
              "categoryId": "k123456789"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

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

<ResponseExample>
  ```json Response theme={null}
  {
    "page": [
      {
        "_id": "j123456789",
        "name": "Mario's Pizza Palace",
        "primaryCategory": "k123456789",
        "categoryName": "Food & Restaurants",
        "logoUrl": "https://storage.convex.dev/mario-logo.jpg",
        "coverImageUrl": "https://storage.convex.dev/mario-cover.jpg",
        "status": "approved",
        "settings": {
          "deliveryEnabled": true,
          "pickupEnabled": true,
          "storeLive": true
        },
        "stats": {
          "averageRating": 4.8,
          "totalReviews": 125,
          "totalOrders": 890,
          "responseTime": "15-30 min"
        },
        "location": {
          "city": "Dubai",
          "area": "Marina"
        },
        "_creationTime": 1640995200000
      },
      {
        "_id": "j987654321",
        "name": "Luigi's Kitchen",
        "primaryCategory": "k123456789",
        "categoryName": "Food & Restaurants",
        "logoUrl": "https://storage.convex.dev/luigi-logo.jpg",
        "status": "approved",
        "settings": {
          "deliveryEnabled": true,
          "pickupEnabled": false,
          "storeLive": true
        },
        "stats": {
          "averageRating": 4.6,
          "totalReviews": 89,
          "totalOrders": 456,
          "responseTime": "20-35 min"
        },
        "location": {
          "city": "Dubai",
          "area": "Downtown"
        },
        "_creationTime": 1640994000000
      }
    ],
    "isDone": false,
    "continueCursor": "eyJfaWQiOiJqOTg3NjU0MzIxIn0",
    "pageStatus": "success",
    "splitCursor": null
  }
  ```
</ResponseExample>

## Get Nearby Stores

Find stores within a specified radius of given coordinates with distance calculation.

<ParamField path="userLatitude" type="number" required>
  User's latitude coordinate (GPS)
</ParamField>

<ParamField path="userLongitude" type="number" required>
  User's longitude coordinate (GPS)
</ParamField>

<ParamField path="radiusKm" type="number">
  Search radius in kilometers (default: platform config default)
</ParamField>

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

<ParamField path="limit" type="number">
  Maximum number of stores to return (default: 20)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Find nearby food stores within 5km
  const nearbyStores = await convex.query(api.customers.explore.getNearbyStores, {
    userLatitude: 25.2048, // Dubai Marina
    userLongitude: 55.2708,
    radiusKm: 5,
    categoryId: "k123456789", // Food category
    limit: 10
  });
  ```

  ```javascript JavaScript theme={null}
  // Find all nearby stores within default radius
  const nearbyStores = await convex.query("customers.explore.getNearbyStores", {
    userLatitude: 25.2048,
    userLongitude: 55.2708
  });
  ```

  ```python Python theme={null}
  response = requests.post(
      f"{CONVEX_URL}/api/query",
      json={
          "path": "customers.explore.getNearbyStores",
          "args": {
              "userLatitude": 25.2048,
              "userLongitude": 55.2708,
              "radiusKm": 3,
              "categoryId": "k123456789"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

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

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "_id": "j123456789",
      "name": "Mario's Pizza Palace",
      "logoUrl": "https://storage.convex.dev/mario-logo.jpg",
      "categoryName": "Food & Restaurants",
      "stats": {
        "averageRating": 4.8,
        "totalReviews": 125
      },
      "settings": {
        "deliveryEnabled": true,
        "storeLive": true
      },
      "location": {
        "city": "Dubai",
        "area": "Marina",
        "coordinates": {
          "latitude": "25.2055",
          "longitude": "55.2710"
        }
      },
      "distance": 0.8
    },
    {
      "_id": "j987654321",
      "name": "Tony's Trattoria",
      "logoUrl": "https://storage.convex.dev/tony-logo.jpg",
      "categoryName": "Food & Restaurants",
      "stats": {
        "averageRating": 4.5,
        "totalReviews": 78
      },
      "settings": {
        "deliveryEnabled": true,
        "storeLive": true
      },
      "location": {
        "city": "Dubai",
        "area": "JBR",
        "coordinates": {
          "latitude": "25.2020",
          "longitude": "55.2690"
        }
      },
      "distance": 2.3
    }
  ]
  ```
</ResponseExample>

## Get Store by ID

Get a specific store by ID for customer viewing. Returns store details with follow status if a customer ID is provided.

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

<ParamField path="customerId" type="Id<'customers'>">
  Optional customer ID to check follow status
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const store = await convex.query(api.customers.explore.getStoreById, {
    storeId: "j123456789",
    customerId: "c123456789" // optional
  });
  ```

  ```javascript JavaScript theme={null}
  const store = await convex.query("customers.explore.getStoreById", {
    storeId: "j123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "_id": "j123456789",
    "name": "Mario's Pizza Palace",
    "primaryCategory": "k123456789",
    "logoUrl": "https://storage.convex.dev/mario-logo.jpg",
    "coverImageUrl": "https://storage.convex.dev/mario-cover.jpg",
    "status": "approved",
    "isActive": true,
    "isFollowingStore": true,
    "ownerName": "Mario Rossi",
    "phoneNumber": "+971501234567",
    "settings": {
      "deliveryEnabled": true,
      "pickupEnabled": true
    },
    "address": {
      "fullAddress": "Dubai Marina, Dubai",
      "latitude": "25.2055",
      "longitude": "55.2710"
    },
    "followersCount": 120,
    "_creationTime": 1640995200000
  }
  ```
</ResponseExample>

## Store Discovery Components

Complete store exploration interface:

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

  interface StoreExplorerProps {
    onStoreSelect: (storeId: string) => void;
  }

  function StoreExplorer({ onStoreSelect }: StoreExplorerProps) {
    const [searchTerm, setSearchTerm] = useState('');
    const [selectedCategory, setSelectedCategory] = useState<string>('');
    const [currentLocation, setCurrentLocation] = useState<{lat: string, lng: string} | null>(null);
    const [viewMode, setViewMode] = useState<'all' | 'nearby'>('all');
    const [cursor, setCursor] = useState<string | null>(null);

    // API queries
    const categories = useQuery(api.shared.categories.getParents);
    
    const stores = useQuery(
      api.customers.explore.getStoresWithPagination,
      viewMode === 'all' ? {
        paginationOpts: { numItems: 20, cursor },
        categoryId: selectedCategory || undefined
      } : "skip"
    );

    const nearbyStores = useQuery(
      api.customers.explore.getNearbyStores,
      viewMode === 'nearby' && currentLocation ? {
        userLatitude: parseFloat(currentLocation.lat),
        userLongitude: parseFloat(currentLocation.lng),
        radiusKm: 10,
        categoryId: selectedCategory || undefined
      } : "skip"
    );

    // Get user location
    useEffect(() => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            setCurrentLocation({
              lat: position.coords.latitude.toString(),
              lng: position.coords.longitude.toString()
            });
          },
          (error) => {
            console.warn('Location access denied:', error);
          }
        );
      }
    }, []);

    const getCurrentStores = () => {
      switch (viewMode) {
        case 'nearby':
          return nearbyStores ? { page: nearbyStores, isDone: true, continueCursor: null } : null;
        default:
          return stores;
      }
    };

    const currentStores = getCurrentStores();

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

    return (
      <div className="max-w-6xl mx-auto space-y-6">
        {/* Header */}
        <div className="text-center">
          <h1 className="text-3xl font-bold text-gray-900">Explore Stores</h1>
          <p className="text-gray-600 mt-2">Discover amazing local businesses in your area</p>
        </div>

        {/* Search and Filters */}
        <div className="bg-white p-6 rounded-lg shadow">
          <div className="flex flex-col md:flex-row gap-4">
            {/* Category Filter */}
            <div className="md:w-64">
              <select
                value={selectedCategory}
                onChange={(e) => {
                  setSelectedCategory(e.target.value);
                  setCursor(null);
                }}
                className="w-full p-3 border border-gray-300 rounded-lg"
              >
                <option value="">All Categories</option>
                {categories?.map((category) => (
                  <option key={category._id} value={category._id}>
                    {category.name}
                  </option>
                ))}
              </select>
            </div>

            {/* View Mode Buttons */}
            <div className="flex space-x-2">
              <button
                onClick={() => {
                  setViewMode('all');
                  setCursor(null);
                }}
                className={`px-4 py-2 rounded ${
                  viewMode === 'all' 
                    ? 'bg-blue-600 text-white' 
                    : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
                }`}
              >
                All Stores
              </button>
              
              {currentLocation && (
                <button
                  onClick={() => {
                    setViewMode('nearby');
                    setCursor(null);
                  }}
                  className={`px-4 py-2 rounded ${
                    viewMode === 'nearby' 
                      ? 'bg-blue-600 text-white' 
                      : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
                  }`}
                >
                  📍 Nearby
                </button>
              )}
            </div>
          </div>
        </div>

        {/* Store Grid */}
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {currentStores?.page.map((store) => (
            <div
              key={store._id}
              onClick={() => onStoreSelect(store._id)}
              className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow cursor-pointer overflow-hidden"
            >
              {/* Store Cover Image */}
              {store.coverImageUrl && (
                <div className="h-32 bg-gray-200">
                  <img
                    src={store.coverImageUrl}
                    alt={store.name}
                    className="w-full h-full object-cover"
                  />
                </div>
              )}

              <div className="p-4">
                {/* Store Header */}
                <div className="flex items-center space-x-3 mb-3">
                  {store.logoUrl && (
                    <img
                      src={store.logoUrl}
                      alt={store.name}
                      className="w-12 h-12 rounded-lg object-cover"
                    />
                  )}
                  <div className="flex-1 min-w-0">
                    <h3 className="font-semibold text-gray-900 truncate">{store.name}</h3>
                    <p className="text-sm text-gray-600">{store.categoryName}</p>
                  </div>
                  <div className="flex items-center space-x-1">
                    <span className="text-yellow-400">⭐</span>
                    <span className="text-sm font-medium">{store.stats.averageRating.toFixed(1)}</span>
                  </div>
                </div>

                {/* Store Info */}
                <div className="space-y-2">
                  <div className="flex items-center justify-between text-sm">
                    <span className="text-gray-600">Reviews:</span>
                    <span className="font-medium">{store.stats.totalReviews}</span>
                  </div>
                  
                  <div className="flex items-center justify-between text-sm">
                    <span className="text-gray-600">Response Time:</span>
                    <span className="font-medium">{store.stats.responseTime}</span>
                  </div>

                  {store.distance !== undefined && (
                    <div className="flex items-center justify-between text-sm">
                      <span className="text-gray-600">Distance:</span>
                      <span className="font-medium">{store.distance.toFixed(1)} km</span>
                    </div>
                  )}

                  <div className="flex items-center justify-between text-sm">
                    <span className="text-gray-600">Location:</span>
                    <span className="font-medium">{store.location.area}, {store.location.city}</span>
                  </div>
                </div>

                {/* Store Status */}
                <div className="flex items-center space-x-2 mt-3">
                  <span className={`px-2 py-1 rounded-full text-xs font-medium ${
                    store.settings.storeLive 
                      ? 'bg-green-100 text-green-800' 
                      : 'bg-gray-100 text-gray-800'
                  }`}>
                    {store.settings.storeLive ? '🟢 Open' : '🔴 Closed'}
                  </span>
                  
                  {store.settings.deliveryEnabled && (
                    <span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
                      🚚 Delivery
                    </span>
                  )}
                  
                  {store.settings.pickupEnabled && (
                    <span className="px-2 py-1 bg-purple-100 text-purple-800 rounded-full text-xs font-medium">
                      📦 Pickup
                    </span>
                  )}
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* Load More Button */}
        {currentStores && !currentStores.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 Stores
            </button>
          </div>
        )}

        {/* Empty State */}
        {currentStores && currentStores.page.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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
              </svg>
            </div>
            <h3 className="text-lg font-medium text-gray-900 mb-2">No stores found</h3>
            <p className="text-gray-600">
              {viewMode === 'nearby'
                ? 'No stores found in your area'
                : 'No stores available in this category'
              }
            </p>
          </div>
        )}
      </div>
    );
  }

  export default StoreExplorer;
  ```

  ```typescript Infinite Scroll Hook theme={null}
  import { useState, useEffect, useCallback } from 'react';
  import { useQuery } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface UseInfiniteStoresProps {
    categoryId?: string;
    pageSize?: number;
  }

  function useInfiniteStores({ categoryId, pageSize = 20 }: UseInfiniteStoresProps = {}) {
    const [allStores, setAllStores] = useState<any[]>([]);
    const [cursor, setCursor] = useState<string | null>(null);
    const [hasMore, setHasMore] = useState(true);
    const [loading, setLoading] = useState(false);

    const stores = useQuery(
      api.customers.explore.getStoresWithPagination,
      !loading ? {
        paginationOpts: { numItems: pageSize, cursor },
        categoryId
      } : "skip"
    );

    useEffect(() => {
      if (stores) {
        setLoading(false);
        
        if (cursor === null) {
          // First load
          setAllStores(stores.page);
        } else {
          // Append new stores
          setAllStores(prev => [...prev, ...stores.page]);
        }
        
        setHasMore(!stores.isDone);
        setCursor(stores.continueCursor);
      }
    }, [stores]);

    const loadMore = useCallback(() => {
      if (hasMore && !loading && stores?.continueCursor) {
        setLoading(true);
        setCursor(stores.continueCursor);
      }
    }, [hasMore, loading, stores?.continueCursor]);

    const reset = useCallback(() => {
      setAllStores([]);
      setCursor(null);
      setHasMore(true);
      setLoading(false);
    }, []);

    return {
      stores: allStores,
      hasMore,
      loading,
      loadMore,
      reset
    };
  }

  // Usage with intersection observer for infinite scroll
  function InfiniteStoreList() {
    const { stores, hasMore, loading, loadMore } = useInfiniteStores({
      pageSize: 12
    });

    const observerRef = useCallback((node: HTMLDivElement) => {
      if (loading) return;
      
      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          loadMore();
        }
      });
      
      if (node) observer.observe(node);
      
      return () => observer.disconnect();
    }, [loading, hasMore, loadMore]);

    return (
      <div>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {stores.map((store, index) => (
            <StoreCard 
              key={store._id} 
              store={store}
              ref={index === stores.length - 1 ? observerRef : undefined}
            />
          ))}
        </div>
        
        {loading && (
          <div className="text-center py-4">
            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
          </div>
        )}
      </div>
    );
  }

  export { StoreExplorer, useInfiniteStores, InfiniteStoreList };
  ```
</CodeGroup>

## Location-based Features

<CardGroup cols={2}>
  <Card title="GPS Discovery" icon="location-dot">
    Find stores near user's current location with distance calculation and radius filtering
  </Card>

  <Card title="Area-based Search" icon="map">
    Browse stores by specific UAE cities and areas for targeted discovery
  </Card>

  <Card title="Delivery Radius" icon="truck">
    Show only stores that deliver to user's location based on delivery zones
  </Card>

  <Card title="Real-time Distance" icon="route">
    Calculate and display real-time distances from user's location to stores
  </Card>
</CardGroup>

## Store Filtering Options

Advanced filtering capabilities:

```typescript theme={null}
interface StoreFilters {
  category: string;           // Filter by business category
  location: {
    city: string;            // Filter by city
    area: string;            // Filter by area
    radius: number;          // Distance radius in km
    coordinates: {
      lat: string;
      lng: string;
    };
  };
  rating: {
    minimum: number;         // Minimum star rating
  };
  features: {
    delivery: boolean;       // Has delivery service
    pickup: boolean;         // Has pickup service
    isOpen: boolean;         // Currently accepting orders
  };
  sorting: 'distance' | 'rating' | 'name' | 'newest' | 'popular';
}
```

## Error Handling

<AccordionGroup>
  <Accordion title="Invalid coordinates">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Invalid GPS coordinates",
      "details": {
        "latitude": "Must be a valid latitude (-90 to 90)",
        "longitude": "Must be a valid longitude (-180 to 180)"
      }
    }
    ```
  </Accordion>

  <Accordion title="No stores found">
    **Status Code**: `200` (Success with empty results)

    ```json theme={null}
    {
      "page": [],
      "isDone": true,
      "continueCursor": null
    }
    ```
  </Accordion>

  <Accordion title="Location service unavailable">
    **Status Code**: `503`

    ```json theme={null}
    {
      "error": "Location service temporarily unavailable",
      "fallback": "Use category browsing instead"
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  Store exploration results are real-time and automatically update when stores change their status, add new products, or update their information.
</Note>

<Tip>
  Use location-based search to provide personalized store recommendations and improve the customer discovery experience.
</Tip>
