Restaurant & Store Finder

This example demonstrates how to create a specialised location finder that searches for specific business types. By using the includedPrimaryTypes parameter, you can build targeted search experiences for restaurants, cafés, shops, and other establishments.

Live Demo

Select a business type below, then search for locations near you. The autocomplete will only show results matching your selected category.

powered by
powered by Google

How It Works

This example demonstrates how to build a specialised location finder by filtering results to specific business types.

  1. Place Type Filtering: Use the includedPrimaryTypes parameter in requestParams to limit results to specific business categories like restaurants, cafés, or pharmacies.
  2. Dynamic Type Switching: Update the search filter on-the-fly using setRequestParams() when users select different business types, providing a seamless filtering experience.
  3. Rich Place Information: The response includes valuable business data such as ratings, review counts, contact information, price levels, and business status that you can display to users.
  4. Enhanced User Experience: By focusing searches on specific business types, you eliminate irrelevant results and help users find exactly what they're looking for more quickly.
  5. Practical Applications: This pattern is perfect for restaurant finders, store locators, service directories, and any application where users need to find specific types of businesses or locations.
<script>
import { PlacesAutocomplete } from 'places-autocomplete-js';

document.addEventListener('DOMContentLoaded', () => {
  let selectedType = 'restaurant';
  let autocomplete;

  // Place type buttons
  const placeTypes = [
    { value: 'restaurant', label: 'Restaurants' },
    { value: 'cafe', label: 'Cafés' },
    { value: 'bar', label: 'Bars' },
    { value: 'bakery', label: 'Bakeries' },
    { value: 'supermarket', label: 'Supermarkets' },
    { value: 'pharmacy', label: 'Pharmacies' },
    { value: 'gas_station', label: 'Petrol Stations' },
    { value: 'bank', label: 'Banks' }
  ];

  // Initialise Places Autocomplete
  autocomplete = new PlacesAutocomplete({
    containerId: 'restaurant-autocomplete-container',
    googleMapsApiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
    onResponse: (placeDetails) => {
      console.log('Place Selected:', placeDetails);
      displayPlaceInfo(placeDetails);
    },
    onError: (error) => {
      console.error('Autocomplete Error:', error.message || error);
    },
    requestParams: {
      includedPrimaryTypes: [selectedType] // Filter by specific business type
    },
    fetchFields: [
      'displayName',
      'formattedAddress',
      'rating',
      'userRatingCount',
      'internationalPhoneNumber',
      'types',
      'priceLevel',
      'businessStatus',
      'websiteURI',
      'regularOpeningHours',
      'editorialSummary'
    ],
    options: {
      placeholder: 'Search for restaurants, cafés, stores...',
      clear_input: false
    }
  });

  // Create place type filter buttons
  const typeContainer = document.getElementById('type-filter-container');
  placeTypes.forEach(type => {
    const button = document.createElement('button');
    button.textContent = type.label;
    button.classList.add('type-button');
    if (type.value === selectedType) {
      button.classList.add('active');
    }
    
    button.addEventListener('click', () => {
      selectedType = type.value;
      
      // Update active button styling
      document.querySelectorAll('.type-button').forEach(btn => {
        btn.classList.remove('active');
      });
      button.classList.add('active');
      
      // Update autocomplete to search for new type
      autocomplete.setRequestParams({
        includedPrimaryTypes: [selectedType]
      });
      
      // Clear the input and results
      autocomplete.clear();
      document.getElementById('place-info').innerHTML = '';
    });
    
    typeContainer.appendChild(button);
  });

  function displayPlaceInfo(placeDetails) {
    const infoContainer = document.getElementById('place-info');
    
    // Format price level
    const formatPriceLevel = (level) => {
      const priceLevels = {
        'INEXPENSIVE': { text: 'Budget-friendly', symbol: '£' },
        'MODERATE': { text: 'Moderate pricing', symbol: '££' },
        'EXPENSIVE': { text: 'Upmarket', symbol: '£££' },
        'VERY_EXPENSIVE': { text: 'Fine dining', symbol: '££££' }
      };
      return priceLevels[level] || { text: level, symbol: '' };
    };

    // Build HTML for place information
    let html = `
      <div class="place-card">
        <div class="place-header">
          <h3>${placeDetails.displayName}</h3>
          ${placeDetails.businessStatus ? 
            `<span class="status ${placeDetails.businessStatus === 'OPERATIONAL' ? 'open' : 'closed'}">
              ${placeDetails.businessStatus === 'OPERATIONAL' ? 'Open' : 'Closed'}
            </span>` : ''}
        </div>
        
        ${placeDetails.editorialSummary ? 
          `<p class="editorial-summary">${placeDetails.editorialSummary}</p>` : ''}
        
        ${placeDetails.types ? 
          `<div class="types">
            ${placeDetails.types
              .filter(t => !['point_of_interest', 'establishment', 'food'].includes(t))
              .slice(0, 5)
              .map(type => `<span class="type-badge">${type.replace(/_/g, ' ')}</span>`)
              .join('')}
          </div>` : ''}
        
        <div class="rating-price">
          ${placeDetails.rating ? 
            `<div class="rating">
              ★ ${placeDetails.rating.toFixed(1)}
              ${placeDetails.userRatingCount ? 
                `<span>(${placeDetails.userRatingCount.toLocaleString()} reviews)</span>` : ''}
            </div>` : ''}
          
          ${placeDetails.priceLevel ? 
            `<div class="price">
              ${formatPriceLevel(placeDetails.priceLevel).text}
              <span>(${formatPriceLevel(placeDetails.priceLevel).symbol})</span>
            </div>` : ''}
        </div>
        
        <div class="contact-details">
          <h4>Contact Details</h4>
          ${placeDetails.formattedAddress ? 
            `<div class="detail">
              <span class="icon">📍</span>
              <span>${placeDetails.formattedAddress}</span>
            </div>` : ''}
          
          ${placeDetails.internationalPhoneNumber ? 
            `<div class="detail">
              <span class="icon">📞</span>
              <a href="tel:${placeDetails.internationalPhoneNumber}">
                ${placeDetails.internationalPhoneNumber}
              </a>
            </div>` : ''}
          
          ${placeDetails.websiteURI ? 
            `<div class="detail">
              <span class="icon">🌐</span>
              <a href="${placeDetails.websiteURI}" target="_blank" rel="noopener noreferrer">
                Visit website
              </a>
            </div>` : ''}
          
          ${placeDetails.id ? 
            `<div class="detail">
              <span class="icon">🧭</span>
              <a href="https://www.google.com/maps/search/?api=1&query=Google&query_place_id=${placeDetails.id}" 
                 target="_blank" rel="noopener noreferrer">
                Get directions
              </a>
            </div>` : ''}
        </div>
        
        ${placeDetails.regularOpeningHours?.weekdayDescriptions ? 
          `<div class="opening-hours">
            <h4>Opening Hours</h4>
            ${placeDetails.regularOpeningHours.weekdayDescriptions
              .map(hours => `<div class="hours">🕐 ${hours}</div>`)
              .join('')}
          </div>` : ''}
      </div>
    `;
    
    infoContainer.innerHTML = html;
  }
});
</script>

<!-- Place Type Filter Buttons -->
<div id="type-filter-container" class="type-filters">
  <!-- Buttons will be dynamically created here -->
</div>

<!-- Autocomplete Search Input -->
<div id="restaurant-autocomplete-container"></div>

<!-- Place Information Display -->
<div id="place-info"></div>

<style>
  .type-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1rem;
  }
  
  .type-button {
    padding: 0.5rem 1rem;
    border: 1px solid #ccc;
    border-radius: 0.5rem;
    background: #f9fafb;
    cursor: pointer;
    transition: all 0.2s;
  }
  
  .type-button.active {
    background: #4f46e5;
    color: white;
    border-color: #4f46e5;
  }
  
  .place-card {
    margin-top: 1.5rem;
    padding: 1.5rem;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    background: white;
  }
  
  .place-header {
    display: flex;
    justify-content: space-between;
    align-items: start;
    margin-bottom: 1rem;
  }
  
  .status {
    padding: 0.25rem 0.75rem;
    border-radius: 0.25rem;
    font-size: 0.875rem;
  }
  
  .status.open {
    background: #dcfce7;
    color: #166534;
  }
  
  .status.closed {
    background: #fee2e2;
    color: #991b1b;
  }
  
  .editorial-summary {
    font-style: italic;
    color: #6b7280;
    margin-bottom: 1rem;
  }
  
  .types {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1rem;
  }
  
  .type-badge {
    padding: 0.25rem 0.75rem;
    background: #eef2ff;
    color: #4f46e5;
    border-radius: 0.25rem;
    font-size: 0.875rem;
  }
  
  .rating-price {
    display: flex;
    gap: 2rem;
    padding: 1rem 0;
    border-top: 1px solid #e5e7eb;
    border-bottom: 1px solid #e5e7eb;
    margin-bottom: 1rem;
  }
  
  .contact-details h4,
  .opening-hours h4 {
    font-size: 0.875rem;
    font-weight: 600;
    text-transform: uppercase;
    color: #6b7280;
    margin-bottom: 0.75rem;
  }
  
  .detail {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
  }
  
  .detail a {
    color: #4f46e5;
    text-decoration: none;
  }
  
  .opening-hours {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid #e5e7eb;
  }
  
  .hours {
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
  }
</style>