Places UI Kit ile yerel arama uygulaması oluşturma

1. Başlamadan önce

Bu codelab'de, Google Haritalar Platformu Places UI Kit'i kullanarak tamamen etkileşimli bir yerel arama uygulaması oluşturmayı öğreneceksiniz.

Tamamlanmış PlaceFinder uygulamasının ekran görüntüsü. New York'un işaretçilerle gösterildiği bir harita, arama sonuçlarının bulunduğu bir kenar çubuğu ve açık bir ayrıntılar kartı gösteriliyor.

Ön koşullar

  • Gerekli API'lerin ve kimlik bilgilerinin yapılandırıldığı bir Google Cloud projesi.
  • HTML ve CSS hakkında temel bilgi
  • Modern JavaScript hakkında bilgi sahibi olmak
  • Chrome'un en yeni sürümü gibi modern bir web tarayıcısı
  • Tercih ettiğiniz bir metin düzenleyici.

Yapacaklarınız

  • JavaScript sınıfı kullanarak bir eşleme uygulaması yapılandırma.
  • Harita görüntülemek için web bileşenlerini kullanma
  • Metin aramasının sonuçlarını gerçekleştirmek ve görüntülemek için Yer Arama Öğesi'ni kullanın.
  • Programlı olarak özel AdvancedMarkerElement harita işaretçileri oluşturun ve yönetin.
  • Kullanıcı bir konum seçtiğinde yer ayrıntıları öğesini gösterin.
  • Dinamik ve kullanıcı dostu bir arayüz oluşturmak için Geocoding API'yi kullanın.

İhtiyacınız olanlar

  • Faturalandırmanın etkin olduğu bir Google Cloud projesi
  • Google Haritalar Platformu API anahtarı
  • Harita kimliği
  • Aşağıdaki API'ler etkinleştirilmiş olmalıdır:
    • Maps JavaScript API
    • Places UI Kit
    • Geocoding API

2. Hazırlanın

Aşağıdaki etkinleştirme adımı için Maps JavaScript API, Places UI Kit ve Geocoding API'yi etkinleştirmeniz gerekir.

Google Haritalar Platformu'nu ayarlama

Henüz bir Google Cloud Platform hesabınız ve faturalandırmanın etkinleştirildiği bir projeniz yoksa lütfen faturalandırma hesabı ve proje oluşturmak için Google Haritalar Platformu'nu Kullanmaya Başlama kılavuzuna bakın.

  1. Cloud Console'da proje açılır menüsünü tıklayın ve bu codelab için kullanmak istediğiniz projeyi seçin.

  1. Bu codelab için gereken Google Haritalar Platformu API'lerini ve SDK'larını Google Cloud Marketplace'te etkinleştirin. Bunun için bu videodaki veya bu dokümandaki adımları uygulayın.
  2. Cloud Console'un Kimlik Bilgileri sayfasında bir API anahtarı oluşturun. Bu videodaki veya bu dokümandaki adımları uygulayabilirsiniz. Google Haritalar Platformu'na yapılan tüm istekler için API anahtarı gerekir.

3. Uygulama kabuğu ve işlevsel bir harita

Bu ilk adımda, uygulamamızın görsel düzenini oluşturacak ve JavaScript'imiz için temiz, sınıfa dayalı bir yapı kuracağız. Bu da üzerine inşa edebileceğimiz sağlam bir temel oluşturur. Bu bölümün sonunda, etkileşimli bir harita gösteren stilize bir sayfanız olacak.

HTML dosyasını oluşturun

Öncelikle index.html adlı bir dosya oluşturun. Bu dosya, başlık, arama filtreleri, kenar çubuğu, harita kapsayıcısı ve gerekli web bileşenleri dahil olmak üzere uygulamamızın tüm yapısını içerir.

Aşağıdaki kodu index.html dosyasına kopyalayın. YOUR_API_KEY_HERE yerine kendi Google Haritalar Platformu API anahtarınızı, DEMO_MAP_ID yerine de kendi Google Haritalar Platformu harita kimliğinizi girdiğinizden emin olun.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Local Search App</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Google Fonts: Roboto -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <!-- GMP Bootstrap Loader -->
    <script>
      (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
        key: "YOUR_API_KEY_HERE",
        v: "weekly",
        libraries: "places,maps,marker,geocoding"
      });
    </script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <!-- Header for search controls -->
    <header class="top-header">
        <div class="logo">
            <svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
            <span>PlaceFinder</span>
        </div>
        <div class="search-container">
            <input
              type="text"
              id="query-input"
              placeholder="e.g., burger in New York"
              value="burger"
            />
            <button id="search-button" aria-label="Search">Search</button>
        </div>
        <div class="filter-container">
            <label class="open-now-label">
              <input type="checkbox" id="open-now-filter"> Open Now
            </label>
            <select id="rating-filter" aria-label="Minimum rating">
              <option value="0" selected>Any rating</option>
              <option value="1">1+ </option>
              <option value="2">2+ ★★</option>
              <option value="3">3+ ★★★</option>
              <option value="4">4+ ★★★★</option>
              <option value="5">5 ★★★★★</option>
            </select>
             <select id="price-filter" aria-label="Price level">
              <option value="0" selected>Any Price</option>
              <option value="1">$</option>
              <option value="2">$$</option>
              <option value="3">$$$</option>
              <option value="4">$$$$</option>
            </select>
        </div>
    </header>

    <!-- Main content area -->
    <div class="app-container">
      <!-- Left Panel: Results -->
      <div class="sidebar">
          <div class="results-header">
            <h2 id="results-header-text">Results</h2>
          </div>
          <div class="results-container">
              <gmp-place-search id="place-search-list" class="hidden" selectable>
                <gmp-place-all-content></gmp-place-all-content>
                <gmp-place-text-search-request></gmp-place-text-search-request>
              </gmp-place-search>

              <div id="placeholder-message" class="placeholder">
                  <p>Your search results will appear here.</p>
              </div>

              <div id="loading-spinner" class="spinner-overlay">
                  <div class="spinner"></div>
              </div>
          </div>
      </div>

      <!-- Right Panel: Map -->
      <div class="map-container">
        <gmp-map
          center="40.758896,-73.985130"
          zoom="13"
          map-id="DEMO_MAP_ID"
        >
        </gmp-map>
        <div id="details-container">
            <gmp-place-details-compact>
                <gmp-place-details-place-request></gmp-place-details-place-request>
                <gmp-place-all-content></gmp-place-all-content>
            </gmp-place-details-compact>
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

CSS dosyasını oluşturma

Ardından, style.css adlı bir dosya oluşturun. Baştan itibaren temiz ve modern bir görünüm oluşturmak için gerekli tüm stil öğelerini şimdi ekleyeceğiz. Bu CSS, genel düzeni, renkleri, yazı tiplerini ve tüm kullanıcı arayüzü öğelerimizin görünümünü yönetir.

Aşağıdaki kodu style.css dosyasına kopyalayın:

/* style.css */
:root {
  --primary-color: #1a73e8;
  --text-color: #202124;
  --text-color-light: #5f6368;
  --background-color: #f8f9fa;
  --panel-background: #ffffff;
  --border-color: #dadce0;
  --shadow-color: rgba(0, 0, 0, 0.1);
}

body {
  font-family: 'Roboto', sans-serif;
  margin: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background-color: var(--background-color);
  color: var(--text-color);
}

.hidden {
  display: none !important;
}

.top-header {
  display: flex;
  align-items: center;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border-color);
  background-color: var(--panel-background);
  gap: 24px;
  flex-shrink: 0;
}

.logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 22px;
  font-weight: 700;
  color: var(--primary-color);
}

.search-container {
  display: flex;
  flex-grow: 1;
  max-width: 720px;
}

.search-container input {
  width: 100%;
  padding: 12px 16px;
  border: 1px solid var(--border-color);
  border-radius: 8px 0 0 8px;
  font-size: 16px;
  transition: box-shadow 0.2s ease;
}

.search-container input:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}

.search-container button {
  padding: 0 20px;
  border: 1px solid var(--primary-color);
  border-radius: 0 8px 8px 0;
  background-color: var(--primary-color);
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: 500;
  transition: background-color 0.2s ease;
}

.search-container button:hover {
  background-color: #185abc;
}

.filter-container {
  display: flex;
  gap: 12px;
  align-items: center;
}

.filter-container select, .open-now-label {
  padding: 10px 14px;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background-color: var(--panel-background);
  font-size: 14px;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.filter-container select:hover, .open-now-label:hover {
  border-color: #c0c2c5;
}

.open-now-label {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

.app-container {
  display: flex;
  flex-grow: 1;
  overflow: hidden;
}

.sidebar {
  width: 35%;
  min-width: 380px;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border-color);
  background-color: var(--panel-background);
  overflow: hidden;
}

.results-header {
  padding: 16px 24px;
  border-bottom: 1px solid var(--border-color);
  flex-shrink: 0;
}

.results-header h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 500;
}

.results-container {
  flex-grow: 1;
  position: relative;
  overflow-y: auto;
  overflow-x: hidden;
}

.placeholder {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 2rem;
  box-sizing: border-box;
}

.placeholder p {
  color: var(--text-color-light);
  font-size: 1.1rem;
}

gmp-place-search {
  width: 100%;
}

.map-container {
  flex-grow: 1;
  position: relative;
}

gmp-map {
  width: 100%;
  height: 100%;
}

.spinner-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
  opacity: 1;
  visibility: visible;
}
.spinner {
  width: 48px;
  height: 48px;
  border: 4px solid #e0e0e0;
  border-top-color: var(--primary-color);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

gmp-place-details-compact {
  width: 350px;
  display: none;
  border: none;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

gmp-place-details-compact::after {
    content: '';
    position: absolute;
    bottom: -12px;
    left: 50%;
    transform: translateX(-50%);
    width: 24px;
    height: 12px;
    background-color: var(--panel-background);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
}

JavaScript uygulama sınıfını oluşturma

Son olarak, script.js adlı bir dosya oluşturun. Uygulamamızı PlaceFinderApp adlı bir JavaScript sınıfı içinde yapılandıracağız. Bu sayede kodumuz düzenli kalır ve durum temiz bir şekilde yönetilir.

Bu ilk kod, sınıfı tanımlar, constructor içindeki tüm HTML öğelerimizi bulur ve Google Haritalar Platformu kitaplıklarını yüklemek için bir init() yöntemi oluşturur.

Aşağıdaki kodu script.js dosyasına kopyalayın:

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // We will add more initialization logic here in later steps.
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

API anahtarı kısıtlamaları

Bu Codelab'in çalışması için API anahtarınıza yeni bir kısıtlama eklemeniz gerekebilir. Bu işlemi nasıl yapacağınızla ilgili daha fazla bilgi ve rehberlik için API anahtarlarınızı kısıtlama başlıklı makaleyi inceleyin.

Çalışmanızı kontrol etme

index.html dosyasını web tarayıcınızda açın. Arama çubuğu ve filtreler içeren bir başlığa sahip bir sayfa, "Arama sonuçlarınız burada gösterilir" mesajını içeren bir kenar çubuğu ve New York City'nin ortasında yer alan büyük bir harita görmeniz gerekir. Bu aşamada arama kontrolleri henüz işlevsel değildir.

4. Arama işlevi uygulama

Bu bölümde, temel arama işlevini uygulayarak uygulamamızı hayata geçireceğiz. Kullanıcı "Ara" düğmesini tıkladığında çalışacak kodu yazacağız. Bu işlevi, kullanıcı etkileşimlerini sorunsuz bir şekilde yönetmek ve yarış durumu gibi yaygın hataları önlemek için en iyi uygulamalara uygun şekilde geliştireceğiz.

Bu adımın sonunda, arama düğmesini tıklayabilir ve uygulama arka planda veri getirirken yükleme animasyonunun görünmesini sağlayabilirsiniz.

Arama yöntemini oluşturma

Öncelikle performSearch yöntemini PlaceFinderApp sınıfımızda tanımlayın. Bu işlev, arama mantığımızın temelini oluşturacak. Ayrıca, "kapı bekçisi" görevi görecek bir örnek değişken olan isSearchInProgress'yı da kullanıma sunacağız. Bu sayede, kullanıcıların halihazırda devam eden bir arama varken yeni bir arama başlatması engellenir. Aksi takdirde hatalar oluşabilir.

performSearch içindeki mantık karmaşık görünebilir. Bu nedenle, mantığı parçalara ayıracağız:

  1. Öncelikle, devam eden bir arama olup olmadığını kontrol eder. Bu durumda herhangi bir işlem yapılmaz.
  2. İşlevi "kilitlemek" için isSearchInProgress bayrağını true olarak ayarlar.
  3. Yükleme animasyonunu gösterir ve kullanıcı arayüzünü yeni sonuçlara hazırlar.
  4. Arama isteğinin textQuery özelliğini null olarak ayarlar. Bu, web bileşeninin yeni bir isteğin geldiğini anlamasını sağlayan önemli bir adımdır.
  5. 0 gecikmeli bir setTimeout kullanır. Bu standart JavaScript tekniği, kodumuzun geri kalanının bir sonraki tarayıcı görevinde çalışmasını planlayarak bileşenin önce null değerini işlemesini sağlar. Kullanıcı aynı şeyi iki kez arasa bile her zaman yeni bir arama tetiklenir.

Etkinlik işleyicileri ekleme

Ardından, kullanıcı uygulamayla etkileşimde bulunduğunda performSearch yöntemimizi çağırmamız gerekir. Tüm etkinlik işleme kodumuzu tek bir yerde tutmak için attachEventListeners adlı yeni bir yöntem oluşturacağız. Şimdilik yalnızca arama düğmesinin click etkinliği için bir işleyici ekleyeceğiz. Ayrıca, bir sonraki adımda kullanacağımız başka bir etkinlik için yer tutucu (gmp-load) ekleyeceğiz.

JavaScript dosyasını güncelleyin

script.js dosyanızı aşağıdaki kodla güncelleyin. Yeni veya değiştirilen bölümler attachEventListeners yöntemi ve performSearch yöntemidir.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // Call the new method to set up listeners
    this.attachEventListeners();
  }

  // NEW: Method to set up all event listeners
  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // We will add the gmp-load listener in the next step
  }

  // NEW: Core search method
  async performSearch() {
    // Exit if a search is already in progress
    if (this.isSearchInProgress) {
      return;
    }
    // Set the lock
    this.isSearchInProgress = true;

    // Show the placeholder and spinner
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // Force a state change by clearing the query first.
    this.searchRequest.textQuery = null;

    // Defer setting the real properties to the next event loop cycle.
    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      // If the query is empty, release the lock and hide the spinner
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // For now, we just set the textQuery. We'll add filters later.
      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  // NEW: Helper method to show/hide the spinner
  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Çalışmanızı kontrol etme

script.js dosyanızı kaydedin ve tarayıcınızda index.html simgesini tıklayarak sayfayı yenileyin. Sayfa, önceki gibi görünmelidir. Şimdi başlıktaki "Ara" düğmesini tıklayın.

Şu iki durum gerçekleşir:

  1. "Arama sonuçlarınız burada gösterilecek" yer tutucu mesajı kaybolur.
  2. Yükleme döner simgesi görünür ve dönmeye devam eder.

Durdurulması için henüz talimat vermediğimizden yükleme simgesi sonsuza kadar dönecek. Sonuçları gösterirken bunu bir sonraki bölümde yapacağız. Bu, arama işlevimizin doğru şekilde tetiklendiğini onaylar.

5. Sonuçları görüntüleme ve işaretçi ekleme

Arama tetikleyici işlevsel hale geldiğine göre, bir sonraki görev sonuçları ekranda göstermektir. Bu bölümdeki kod, arama mantığını kullanıcı arayüzüne bağlar. Yer Arama Öğesi, verileri yüklemeyi tamamladığında arama "kilidini" açar, yükleme spinner'ını gizler ve her sonuç için haritada bir işaretçi gösterir.

Arama tamamlama önerilerini dinleme

Yer Arama Öğesi, verileri başarıyla getirdiğinde gmp-load etkinliğini tetikler. Bu, sonuçları işlememiz için mükemmel bir sinyaldir.

Öncelikle attachEventListeners yöntemimizde bu etkinlik için bir etkinlik işleyici ekleyin.

İşaretçi işleme yöntemleri oluşturma

Ardından, clearMarkers ve addMarkers olmak üzere iki yeni yardımcı yöntem oluşturacağız.

  • clearMarkers(), önceki aramadaki tüm işaretleri kaldırır.
  • addMarkers(), gmp-load dinleyicimiz tarafından aranacak. Aramanın döndürdüğü yerler listesinde döngü oluşturur ve her biri için yeni bir AdvancedMarkerElement oluşturur. Arama döngüsünü tamamlamak için yükleme animasyonunu da burada gizleyip isSearchInProgress kilidini açarız.

İşaretçileri, anahtar olarak yer kimliğini kullanarak bir nesnede (this.markers) sakladığımızı unutmayın. Bu, işaretçileri yönetmenin bir yoludur ve daha sonra belirli bir işaretçiyi bulmamıza olanak tanır.

Son olarak, her yeni aramanın başında clearMarkers() işlevini çağırmamız gerekir. Bu işlem için en uygun yer performSearch'dır.

JavaScript dosyasını güncelleyin

script.js dosyanızı yeni yöntemlerle ve attachEventListeners ile performSearch'deki değişikliklerle güncelleyin.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // NEW: Listen for when the search component has loaded results
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
  }

  // NEW: Method to clear markers from a previous search
  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // NEW: Method to add markers for new search results
  addMarkers() {
    // Release the lock and hide the spinner
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    // Create a new marker for each place result
    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Store marker by its place ID for access later
      this.markers[place.id] = marker;
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // NEW: Clear old markers before starting a new search
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Çalışmanızı kontrol etme

Dosyalarınızı kaydedin ve tarayıcınızda sayfayı yenileyin. "Ara" düğmesini tıklayın.

Yükleme döner simgesi kısa bir süre görünüp kaybolur. Kenar çubuğu, arama terimiyle alakalı yerlerin listesiyle doldurulur ve haritada ilgili işaretçiler gösterilir. İşaretçiler henüz tıklandığında herhangi bir işlem yapmıyor. Bu etkileşimi bir sonraki bölümde ekleyeceğiz.

6. Arama filtrelerini ve liste etkileşimini etkinleştirme

Uygulamamız artık arama sonuçlarını gösterebiliyor ancak henüz etkileşimli değil. Bu bölümde, tüm kullanıcı kontrollerini hayata geçireceğiz. Filtreleri etkinleştirir, "Enter" tuşuyla aramayı etkinleştirir ve sonuç listesindeki öğeleri haritadaki ilgili konumlarına bağlarız.

Bu adımın sonunda uygulama, kullanıcı girişine tamamen yanıt veriyormuş gibi görünür.

Arama filtrelerini etkinleştirme

İlk olarak, performSearch yöntemi, üstbilgideki tüm filtre kontrollerinden değerleri okuyacak şekilde güncellenecek. Her filtre (fiyat, puan ve "Şu anda açık") için arama yürütülmeden önce searchRequest nesnesinde ilgili özellik ayarlanır.

Tüm kontroller için etkinlik işleyicileri ekleme

Ardından, attachEventListeners yöntemimizi genişleteceğiz. Her filtre kontrolüne change etkinliği için işleyiciler ve kullanıcı "Enter" tuşuna bastığında bunu algılamak için arama girişine keydown işleyicisi ekleyeceğiz. Bu yeni dinleyicilerin tümü performSearch yöntemini çağırır.

Sonuç listesini haritaya bağlama

Sorunsuz bir deneyim için kenar çubuğundaki sonuç listesinde bir öğeyi tıkladığınızda harita o konuma odaklanmalıdır.

Yeni bir yöntem olan handleResultClick, bir öğe tıklandığında Yer Arama Öğesi tarafından tetiklenen gmp-select etkinliğini dinler. Bu işlev, ilişkili yerin konumunu bulur ve haritayı sorunsuz bir şekilde bu konuma kaydırır.

Bu özelliğin çalışması için selectable özelliğinin index.html içindeki gmp-place-search bileşeninizde bulunduğundan emin olun.

<gmp-place-search id="place-search-list" class="hidden" selectable>
    <gmp-place-all-content></gmp-place-all-content>
    <gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>

JavaScript dosyasını güncelleyin

script.js dosyanızı aşağıdaki tam kodla güncelleyin. Bu sürümde yeni handleResultClick yöntemi ve attachEventListeners ile performSearch'deki güncellenmiş mantık yer almaktadır.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  // UPDATED: All event listeners are now attached
  attachEventListeners() {
    // Listen for the 'Enter' key press in the search input
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });

    // Listen for a sidebar result click
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));

    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      this.markers[place.id] = marker;
    }
  }

  // NEW: Function to handle clicks on the results list
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.location) return;
    // Pan the map to the selected place
    this.map.panTo(place.location);
  }

  // UPDATED: Search function now includes all filters
  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      // Add filter values to the request
      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Çalışmanızı kontrol etme

script.js dosyanızı kaydedin ve sayfayı yenileyin. Uygulama artık oldukça etkileşimli olmalıdır.

Aşağıdakileri doğrulayın:

  • Arama kutusunda "Enter" tuşuna basarak arama yapabilirsiniz.
  • Filtrelerden herhangi birinin (fiyat, puan, şu anda açık) değiştirilmesi yeni bir arama başlatır ve sonuçları günceller.
  • Kenar çubuğundaki sonuç listesinde bir öğeyi tıkladığınızda harita artık sorunsuz bir şekilde o öğenin konumuna kaydırılıyor.

Bir sonraki bölümde, bir işaretçi tıklandığında görünen ayrıntılar kartını uygulayacağız.

7. Yer Ayrıntıları öğesini uygulama

Uygulamamız artık tamamen etkileşimli ancak önemli bir özellik eksik: Seçilen bir yer hakkında daha fazla bilgi görme özelliği. Bu bölümde, kullanıcı haritadaki bir işaretçiyi tıkladığında veya Yer Arama Öğesi'nde bir öğe seçtiğinde görünecek olan Yer Ayrıntıları Öğesi'ni uygulayacağız.

Yeniden kullanılabilir ayrıntılar kartı kapsayıcısı oluşturma

Yer ayrıntılarını haritada göstermenin en verimli yolu, tek bir yeniden kullanılabilir kapsayıcı oluşturmaktır. Bu kapsayıcı olarak AdvancedMarkerElement kullanacağız. İçeriği, index.html içinde zaten bulunan gizli gmp-place-details-compact widget'ı olacaktır.

Bu yeniden kullanılabilir işaretçinin oluşturulması için yeni bir yöntem olan initDetailsPopup kullanılacak. Uygulama yüklendiğinde bir kez oluşturulur ve gizli olarak başlatılır. Bu yöntemde, haritanın herhangi bir yerini tıkladığınızda ayrıntılar kartının gizlenmesi için ana haritaya bir dinleyici de ekleyeceğiz.

İşaretçi tıklama davranışını güncelleme

Ardından, kullanıcının bir yer işaretçisini tıkladığında ne olacağını güncellememiz gerekir. addMarkers yöntemi içindeki 'click' dinleyicisi artık ayrıntılar kartını göstermekten sorumlu olacak.

Bir işaretçi tıklandığında işleyici:

  1. Haritayı işaretçinin bulunduğu konuma kaydırın.
  2. Ayrıntılar kartını, söz konusu yere ait bilgilerle güncelleyin.
  3. Ayrıntılar kartını işaretçinin konumuna yerleştirin ve görünür hale getirin.

Liste tıklamasını işaretçi tıklamasına bağlama

Son olarak, handleResultClick yöntemini güncelleyeceğiz. Artık yalnızca haritada kaydırma yapmak yerine, ilgili işaretçide click etkinliği programatik olarak tetiklenecek. Bu güçlü kalıp, her iki etkileşim için de aynı mantığı yeniden kullanmamıza olanak tanıyarak kodumuzu temiz ve bakımı kolay hale getirir.

JavaScript dosyasını güncelleyin

script.js dosyanızı aşağıdaki kodla güncelleyin. Yeni veya değiştirilen bölümler initDetailsPopup yöntemi ile güncellenen addMarkers ve handleResultClick yöntemleridir.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // NEW: Call the method to initialize the details card
    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  // NEW: Method to set up the reusable details card
  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // UPDATED: The marker's click listener now shows the details card
  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Add the click listener to show the details card
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  // UPDATED: This now triggers the marker's click event
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      // Programmatically trigger the marker's click event
      marker.click();
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    // Hide the details card when a new search starts
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Çalışmanızı kontrol etme

script.js dosyanızı kaydedin ve sayfayı yenileyin. Uygulama artık isteğe bağlı olarak ayrıntıları gösterecektir.

Aşağıdakileri doğrulayın:

  • Haritadaki bir işaretçiyi tıkladığınızda harita ortalanır ve işaretçinin üzerinde stilize edilmiş bir ayrıntılar kartı açılır.
  • Kenar çubuğundaki sonuç listesinde bir öğeyi tıklamak da aynı işlemi yapar.
  • Haritada kartın dışında bir yeri tıkladığınızda kart kapanır.
  • Yeni bir arama başlatmak da açık olan tüm ayrıntılar kartlarını kapatır.

8. Son rötuşları yapma

Uygulamamız artık tamamen işlevsel ancak kullanıcı deneyimini daha da iyileştirmek için ekleyebileceğimiz birkaç son dokunuş var. Bu son bölümde iki temel özelliği uygulayacağız: arama sonuçları için daha iyi bir bağlam sağlayan dinamik bir başlık ve kullanıcının arama sorgusu için otomatik biçimlendirme.

Dinamik sonuç başlığı oluşturma

Şu anda kenar çubuğu üstbilgisinde her zaman "Sonuçlar" yazıyor. Mevcut aramayı yansıtacak şekilde güncelleyerek bu bilgiyi daha bilgilendirici hale getirebiliriz. Örneğin, "New York yakınlarındaki hamburgerciler".

Bunu yapmak için Haritalar'ın merkez koordinatlarını şehir adı gibi okunabilir bir konuma dönüştürmek üzere Geocoding API'yi kullanacağız. Bu mantık, yeni bir async yöntemi olan updateResultsHeader tarafından işlenir. Her arama yapıldığında çağrılır.

Kullanıcının arama sorgusunu biçimlendirme

Kullanıcı arayüzünün temiz ve tutarlı görünmesini sağlamak için kullanıcının arama terimini otomatik olarak "Başlık Büyük Harf" biçiminde (ör. "burger restaurant" ifadesi "Burger Restaurant" olarak değiştirilir). Bu dönüşüm, toTitleCase yardımcı işleviyle gerçekleştirilir. performSearch yöntemi, arama yapmadan ve başlığı güncellemeden önce kullanıcının girişinde bu işlevi kullanacak şekilde güncellenecek.

JavaScript dosyasını güncelleyin

script.js dosyanızı kodun son sürümüyle güncelleyin. Buna yeni toTitleCase ve updateResultsHeader yöntemleri ile bunları entegre eden güncellenmiş performSearch yöntemi dahildir.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  // NEW: Helper function to format text to Title Case
  toTitleCase(str) {
    if (!str) return '';
    return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) { marker.map = null; }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      marker.click();
    }
  }

  // UPDATED: Now integrates formatting and the dynamic header
  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // Format the query and update the input box value
      const formattedQuery = this.toTitleCase(rawQuery);
      this.queryInput.value = formattedQuery;

      // Update the header with the new query and location
      await this.updateResultsHeader(formattedQuery);

      // Pass the formatted query to the search request
      this.searchRequest.textQuery = formattedQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  // NEW: Method to update the sidebar header with geocoded location
  async updateResultsHeader(query) {
    try {
      const response = await this.geocoder.geocode({ location: this.map.getCenter() });
      if (response.results && response.results.length > 0) {
        const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
        const city = cityResult.address_components[0].long_name;
        this.resultsHeaderText.textContent = `${query} near ${city}`;
      } else {
        this.resultsHeaderText.textContent = `${query} near current map area`;
      }
    } catch (error) {
      console.error("Geocoding failed:", error);
      this.resultsHeaderText.textContent = `Results for ${query}`;
    }
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Çalışmanızı kontrol etme

script.js dosyanızı kaydedin ve sayfayı yenileyin.

Özellikleri doğrulayın:

  • Arama kutusuna pizza (tamamı küçük harflerle) yazıp Ara'yı tıklayın. Kutudaki metin "Pizza" olarak değişmeli ve kenar çubuğundaki başlık "New York yakınlarındaki pizzalar" olarak güncellenmelidir.
  • Haritayı Boston gibi farklı bir şehre kaydırın ve tekrar arama yapın. Başlık "Boston yakınlarındaki pizzacılar" olarak güncellenir.

9. Tebrikler

Places UI Kit'in basitliğini temel Google Haritalar Platformu JavaScript API'lerinin gücüyle birleştiren eksiksiz ve etkileşimli bir yerel arama uygulamasını başarıyla oluşturdunuz.

Öğrendikleriniz

  • Durumu ve mantığı yönetmek için JavaScript sınıfı kullanarak bir harita uygulaması nasıl yapılandırılır?
  • Hızlı kullanıcı arayüzü geliştirme için Places UI Kit'i Google Haritalar JavaScript API ile kullanma
  • Haritada özel ilgi alanlarını göstermek için Gelişmiş İşaretçiler'i programatik olarak ekleme ve yönetme
  • Daha iyi bir kullanıcı deneyimi için Coğrafi Kodlama Hizmeti'ni kullanarak koordinatları okunabilir adreslere dönüştürme
  • Durum işaretlerini kullanarak ve bileşen özelliklerinin doğru şekilde güncellendiğinden emin olarak etkileşimli bir uygulamadaki yaygın yarış durumlarını tanımlama ve düzeltme.

Sırada ne var?

  • Renklerini ve ölçeklerini değiştirerek veya özel HTML kullanarak gelişmiş işaretçileri özelleştirme hakkında daha fazla bilgi edinin.
  • Haritanızın görünümünü ve tarzını markanıza uygun şekilde özelleştirmek için Bulut Tabanlı Harita Stilleri'ni keşfedin.
  • Kullanıcıların arama alanlarını tanımlamak için haritada şekil çizmelerine izin vermek üzere Çizim Kitaplığı'nı eklemeyi deneyin.
  • Aşağıdaki anketi yanıtlayarak en yararlı bulacağınız içerikleri oluşturmamıza yardımcı olun:

Başka hangi codelab'leri görmek istersiniz?

Haritalarda veri görselleştirme Haritalarımın stilini özelleştirme hakkında daha fazla bilgi Haritalarda 3D etkileşimler için geliştirme

En çok ilgilendiğiniz codelab'i bulamıyor musunuz? Buradan yeni bir sorunla ilgili istekte bulunun.