जगहों की जानकारी देने वाली यूआई किट का इस्तेमाल करके, स्थानीय खोज वाला ऐप्लिकेशन बनाना

1. शुरू करने से पहले

इस कोडलैब में, Google Maps Platform की Places UI Kit का इस्तेमाल करके, पूरी तरह से इंटरैक्टिव लोकल सर्च ऐप्लिकेशन बनाने का तरीका बताया गया है.

PlaceFinder ऐप्लिकेशन का स्क्रीनशॉट. इसमें न्यूयॉर्क का मैप दिख रहा है. साथ ही, मार्कर, खोज के नतीजों वाला साइडबार, और जानकारी वाला कार्ड खुला हुआ दिख रहा है.

ज़रूरी शर्तें

  • ज़रूरी एपीआई और क्रेडेंशियल के साथ कॉन्फ़िगर किया गया Google Cloud प्रोजेक्ट.
  • एचटीएमएल और सीएसएस की बुनियादी जानकारी.
  • मॉडर्न JavaScript की जानकारी.
  • कोई मॉडर्न वेब ब्राउज़र, जैसे कि Chrome का नया वर्शन.
  • अपनी पसंद का टेक्स्ट एडिटर.

आपको क्या करना होगा

  • JavaScript क्लास का इस्तेमाल करके, मैपिंग ऐप्लिकेशन को स्ट्रक्चर करें.
  • मैप दिखाने के लिए वेब कॉम्पोनेंट का इस्तेमाल करना
  • टेक्स्ट खोज करने और उसके नतीजे दिखाने के लिए, जगह की खोज करने वाले एलिमेंट का इस्तेमाल करें.
  • प्रोग्राम के हिसाब से, कस्टम AdvancedMarkerElement मैप मार्कर बनाना और उन्हें मैनेज करना.
  • जब कोई उपयोगकर्ता किसी जगह को चुनता है, तब 'जगह की जानकारी दिखाएं' एलिमेंट को दिखाएं.
  • डाइनैमिक और इस्तेमाल में आसान इंटरफ़ेस बनाने के लिए, Geocoding API का इस्तेमाल करें.

आपको किन चीज़ों की ज़रूरत होगी

  • बिलिंग की सुविधा वाला Google Cloud प्रोजेक्ट
  • Google Maps Platform API पासकोड
  • मैप आईडी
  • ये एपीआई चालू किए गए हैं:
    • Maps JavaScript एपीआई
    • Places UI Kit
    • जियोकोडिंग एपीआई

2. सेट अप करें

यहां दिया गया तरीका पूरा करने के लिए, आपको Maps JavaScript API, Places UI Kit, और Geocoding API चालू करना होगा.

Google Maps Platform सेट अप करना

अगर आपके पास Google Cloud Platform खाता और बिलिंग की सुविधा वाला प्रोजेक्ट नहीं है, तो बिलिंग की सुविधा वाला खाता और प्रोजेक्ट बनाएं. ऐसा करने का तरीका जानने के लिए, कृपया Google Maps Platform का इस्तेमाल शुरू करना देखें.

  1. Cloud Console में, प्रोजेक्ट वाले ड्रॉप-डाउन मेन्यू पर क्लिक करें. इसके बाद, उस प्रोजेक्ट को चुनें जिसे इस कोडलैब के लिए इस्तेमाल करना है.

  1. इस कोडलैब के लिए ज़रूरी Google Maps Platform API और एसडीके को Google Cloud Marketplace में जाकर चालू करें. ऐसा करने के लिए, इस वीडियो या दस्तावेज़ में बताया गया तरीका अपनाएं.
  2. Cloud Console के क्रेडेंशियल पेज पर जाकर, एक एपीआई पासकोड जनरेट करें. ऐसा करने के लिए, इस वीडियो या दस्तावेज़ में बताया गया तरीका अपनाएं. Google Maps Platform का इस्तेमाल करने के लिए, एपीआई पासकोड ज़रूरी है.

3. ऐप्लिकेशन शेल और काम करने वाला मैप

इस पहले चरण में, हम अपने ऐप्लिकेशन के लिए पूरा विज़ुअल लेआउट बनाएंगे. साथ ही, अपनी JavaScript के लिए क्लास पर आधारित एक साफ़-सुथरा स्ट्रक्चर तैयार करेंगे. इससे हमें आगे काम करने के लिए एक मज़बूत आधार मिलता है. इस सेक्शन के आखिर तक, आपके पास स्टाइल किया गया एक ऐसा पेज होगा जिस पर इंटरैक्टिव मैप दिखेगा.

एचटीएमएल फ़ाइल बनाना

सबसे पहले, index.html नाम की फ़ाइल बनाएं. इस फ़ाइल में, हमारे ऐप्लिकेशन का पूरा स्ट्रक्चर मौजूद होगा. इसमें हेडर, खोज फ़िल्टर, साइडबार, मैप कंटेनर, और ज़रूरी वेब कॉम्पोनेंट शामिल हैं.

नीचे दिए गए कोड को index.html में कॉपी करें. YOUR_API_KEY_HERE को अपने Google Maps Platform API पासकोड से और DEMO_MAP_ID को अपने Google Maps Platform मैप आईडी से बदलना न भूलें.

<!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>

सीएसएस फ़ाइल बनाना

इसके बाद, style.css नाम की फ़ाइल बनाएं. हम अब सभी ज़रूरी स्टाइलिंग जोड़ेंगे, ताकि शुरू से ही एक साफ़-सुथरा और मॉडर्न लुक मिल सके. यह सीएसएस, हमारे सभी यूआई एलिमेंट के लेआउट, रंगों, फ़ॉन्ट, और लुक को मैनेज करती है.

नीचे दिए गए कोड को style.css में कॉपी करें:

/* 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 ऐप्लिकेशन क्लास बनाना

आखिर में, script.js नाम की एक फ़ाइल बनाएं. हम अपने ऐप्लिकेशन को PlaceFinderApp नाम की JavaScript क्लास में स्ट्रक्चर करेंगे. इससे हमारा कोड व्यवस्थित रहता है और स्टेट को सही तरीके से मैनेज किया जाता है.

यह शुरुआती कोड, क्लास को तय करेगा. साथ ही, constructor में मौजूद हमारे सभी एचटीएमएल एलिमेंट को ढूंढ लेगा. इसके अलावा, Google Maps Platform की लाइब्रेरी लोड करने के लिए, init() मेथड बनाएगा.

नीचे दिए गए कोड को script.js में कॉपी करें:

// 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();
});

एपीआई कुंजी से जुड़ी पाबंदियां

इस कोडलैब को काम करने के लिए, आपको अपनी एपीआई कुंजी पर नई पाबंदी लगानी पड़ सकती है. ज़्यादा जानकारी और ऐसा करने के तरीके के बारे में जानने के लिए, अपने एपीआई पासकोड को सीमित करें लेख पढ़ें.

अपने काम की जांच करना

अपने वेब ब्राउज़र में index.html फ़ाइल खोलें. आपको एक पेज दिखेगा. इसमें हेडर में खोज बार और फ़िल्टर होंगे. साथ ही, साइडबार में "खोज के नतीजे यहां दिखेंगे" मैसेज होगा. इसके अलावा, पेज पर न्यू यॉर्क सिटी के बीच में एक बड़ा मैप होगा. इस चरण में, खोज के कंट्रोल अभी काम नहीं कर रहे हैं.

4. खोज की सुविधा लागू करना

इस सेक्शन में, हम खोज से जुड़ी मुख्य सुविधा को लागू करके अपने ऐप्लिकेशन को बेहतर बनाएंगे. हम ऐसा कोड लिखेंगे जो उपयोगकर्ता के "खोजें" बटन पर क्लिक करने पर काम करेगा. हम इस फ़ंक्शन को शुरू से ही सबसे सही तरीकों के साथ बनाएंगे, ताकि उपयोगकर्ता के इंटरैक्शन को आसानी से मैनेज किया जा सके. साथ ही, रेस कंडीशन जैसे सामान्य बग को रोका जा सके.

इस चरण के आखिर तक, आपके पास खोज बटन पर क्लिक करने का विकल्प होगा. इसके बाद, आपको लोडिंग स्पिनर दिखेगा. ऐसा तब होगा, जब ऐप्लिकेशन बैकग्राउंड में डेटा फ़ेच कर रहा होगा.

खोज का तरीका बनाना

सबसे पहले, हमारी PlaceFinderApp क्लास में performSearch तरीके को तय करें. यह फ़ंक्शन, खोज के लॉजिक का मुख्य हिस्सा होगा. हम एक इंस्टेंस वैरिएबल, isSearchInProgress भी लॉन्च करेंगे, जो "गेटकीपर" के तौर पर काम करेगा. इससे उपयोगकर्ता को एक साथ दो खोज शुरू करने से रोका जाता है. ऐसा करने पर गड़बड़ियां हो सकती हैं.

performSearch में मौजूद लॉजिक थोड़ा मुश्किल लग सकता है. इसलिए, हम इसे आसान शब्दों में समझाएंगे:

  1. यह सबसे पहले यह देखता है कि कोई खोज पहले से चल रही है या नहीं. अगर ऐसा है, तो कुछ नहीं होता.
  2. यह फ़ंक्शन को "लॉक" करने के लिए, isSearchInProgress फ़्लैग को true पर सेट करता है.
  3. यह लोडिंग स्पिनर दिखाता है और नए नतीजों के लिए यूज़र इंटरफ़ेस (यूआई) तैयार करता है.
  4. यह खोज के अनुरोध की textQuery प्रॉपर्टी को null पर सेट करता है. यह एक ज़रूरी चरण है. इससे वेब कॉम्पोनेंट को यह पता चलता है कि नया अनुरोध आ रहा है.
  5. यह 0 की देरी के साथ setTimeout का इस्तेमाल करता है. JavaScript की इस स्टैंडर्ड तकनीक की मदद से, हमारे बाकी कोड को अगले ब्राउज़र टास्क में चलाने के लिए शेड्यूल किया जाता है. इससे यह पक्का होता है कि कॉम्पोनेंट ने सबसे पहले null वैल्यू को प्रोसेस किया है. अगर उपयोगकर्ता एक ही चीज़ को दो बार खोजता है, तब भी नई खोज ट्रिगर होगी.

इवेंट लिसनर जोड़ना

इसके बाद, जब उपयोगकर्ता ऐप्लिकेशन से इंटरैक्ट करे, तब हमें performSearch तरीके को कॉल करना होगा. हम एक नया तरीका, attachEventListeners बनाएंगे, ताकि इवेंट हैंडलिंग से जुड़े हमारे सभी कोड एक ही जगह पर रहें. फ़िलहाल, हम सिर्फ़ खोज बटन के click इवेंट के लिए लिसनर जोड़ेंगे. हम एक और इवेंट, gmp-load के लिए प्लेसहोल्डर भी जोड़ेंगे. इसका इस्तेमाल हम अगले चरण में करेंगे.

JavaScript फ़ाइल अपडेट करना

नीचे दिए गए कोड का इस्तेमाल करके, अपनी script.js फ़ाइल को अपडेट करें. attachEventListeners और performSearch सेक्शन में बदलाव किया गया है या इन्हें नया जोड़ा गया है.

// 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();
});

अपने काम की जांच करना

अपनी script.js फ़ाइल सेव करें और अपने ब्राउज़र में index.html को रीफ़्रेश करें. पेज पहले जैसा ही दिखना चाहिए. अब हेडर में मौजूद "खोजें" बटन पर क्लिक करें.

आपको ये दो चीज़ें दिखेंगी:

  1. "आपके खोज नतीजे यहां दिखेंगे" वाला प्लेसहोल्डर मैसेज हट जाता है.
  2. लोडिंग स्पिनर दिखता है और घूमता रहता है.

स्पिनर हमेशा घूमता रहेगा, क्योंकि हमने इसे अभी तक यह नहीं बताया है कि इसे कब रुकना है. हम अगले सेक्शन में नतीजे दिखाते समय ऐसा करेंगे. इससे पुष्टि होती है कि खोज करने की सुविधा सही तरीके से ट्रिगर हो रही है.

5. नतीजे दिखाना और मार्कर जोड़ना

खोज ट्रिगर के काम करने के बाद, अगला काम स्क्रीन पर नतीजे दिखाना है. इस सेक्शन में मौजूद कोड, खोज के लॉजिक को यूज़र इंटरफ़ेस (यूआई) से कनेक्ट करेगा. जगह की जानकारी खोजने वाला एलिमेंट, डेटा लोड करने की प्रोसेस पूरी होने के बाद, खोज के "लॉक" को हटा देगा. साथ ही, लोडिंग स्पिनर को छिपा देगा और हर नतीजे के लिए मैप पर मार्कर दिखाएगा.

खोज पूरी होने की सूचना सुनना

जब Place Search Element, डेटा को फ़ेच कर लेता है, तब वह gmp-load इवेंट को ट्रिगर करता है. यह हमारे लिए नतीजों को प्रोसेस करने का सबसे सही सिग्नल है.

सबसे पहले, हमारे attachEventListeners तरीके में इस इवेंट के लिए एक इवेंट लिसनर जोड़ें.

मार्कर मैनेज करने के तरीके बनाना

इसके बाद, हम दो नए हेल्पर तरीके बनाएंगे: clearMarkers और addMarkers.

  • clearMarkers() से, पिछली खोज के सभी मार्कर हट जाएंगे.
  • addMarkers() को हमारे gmp-load श्रोता कॉल करेंगे. यह खोज के नतीजों में मिली जगहों की सूची में लूप करेगा और हर जगह के लिए एक नया AdvancedMarkerElement बनाएगा. खोज के नतीजे लोड होने के दौरान दिखने वाले स्पिनर को भी यहीं छिपाया जाएगा. साथ ही, isSearchInProgress को अनलॉक किया जाएगा. इससे खोज की प्रोसेस पूरी हो जाएगी.

ध्यान दें कि हम मार्कर को किसी ऑब्जेक्ट (this.markers) में सेव कर रहे हैं. इसके लिए, Place ID को कुंजी के तौर पर इस्तेमाल किया जा रहा है. यह मार्कर मैनेज करने का तरीका है. इससे हमें बाद में किसी मार्कर को ढूंढने में मदद मिलेगी.

आखिर में, हमें हर नई खोज की शुरुआत में clearMarkers() को कॉल करना होगा. इसके लिए सबसे सही जगह performSearch है.

JavaScript फ़ाइल अपडेट करना

script.js फ़ाइल को नए तरीकों के साथ अपडेट करें. साथ ही, attachEventListeners और performSearch में हुए बदलावों को भी अपडेट करें.

// 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();
});

अपने काम की जांच करना

अपनी फ़ाइलें सेव करें और ब्राउज़र में पेज को रीफ़्रेश करें. "खोजें" बटन पर क्लिक करें.

अब कुछ समय के लिए लोडिंग स्पिनर दिखेगा और फिर गायब हो जाएगा. साइडबार में, खोजे गए शब्द से जुड़ी जगहों की सूची दिखेगी. साथ ही, आपको मैप पर उनसे जुड़े मार्कर दिखेंगे. फ़िलहाल, मार्कर पर क्लिक करने से कोई कार्रवाई नहीं होती. हम अगले सेक्शन में इंटरैक्टिविटी जोड़ेंगे.

6. खोज के फ़िल्टर और सूची में इंटरैक्टिविटी की सुविधा चालू करना

हमारा ऐप्लिकेशन अब खोज के नतीजे दिखा सकता है, लेकिन यह अभी इंटरैक्टिव नहीं है. इस सेक्शन में, हम उपयोगकर्ता के सभी कंट्रोल को लाइव करेंगे. हम फ़िल्टर चालू करेंगे, "Enter" कुंजी की मदद से खोजने की सुविधा चालू करेंगे, और नतीजों की सूची में मौजूद आइटम को मैप पर उनकी जगह से कनेक्ट करेंगे.

इस चरण के आखिर तक, ऐप्लिकेशन उपयोगकर्ता के इनपुट पर पूरी तरह से रिस्पॉन्स करेगा.

सर्च फ़िल्टर चालू करना

सबसे पहले, performSearch तरीके को अपडेट किया जाएगा, ताकि हेडर में मौजूद सभी फ़िल्टर कंट्रोल से वैल्यू पढ़ी जा सकें. हर फ़िल्टर (कीमत, रेटिंग, और "अभी खुला है") के लिए, खोज शुरू होने से पहले searchRequest ऑब्जेक्ट पर उससे जुड़ी प्रॉपर्टी सेट की जाएगी.

सभी कंट्रोल के लिए इवेंट लिसनर जोड़ना

इसके बाद, हम attachEventListeners तरीके को बेहतर बनाएंगे. हम हर फ़िल्टर कंट्रोल पर change इवेंट के लिए लिसनर जोड़ेंगे. साथ ही, खोज के लिए इस्तेमाल किए जाने वाले इनपुट पर keydown लिसनर जोड़ेंगे, ताकि यह पता चल सके कि उपयोगकर्ता ने "Enter" कुंजी कब दबाई. ये सभी नए लिसनर, performSearch तरीके को कॉल करेंगे.

नतीजों की सूची को मैप से कनेक्ट करना

बेहतर अनुभव देने के लिए, साइडबार में मौजूद नतीजों की सूची में किसी आइटम पर क्लिक करने से, मैप को उस जगह पर फ़ोकस करना चाहिए.

नया तरीका, handleResultClick, gmp-select इवेंट को सुनेगा. यह इवेंट, किसी आइटम पर क्लिक करने पर Place Search Element ट्रिगर करता है. इस फ़ंक्शन से, जगह की जानकारी मिल जाएगी और मैप को उस जगह पर आसानी से पैन किया जा सकेगा.

इसके लिए, पक्का करें कि index.html में मौजूद gmp-place-search कॉम्पोनेंट पर selectable एट्रिब्यूट मौजूद हो.

<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 फ़ाइल अपडेट करना

script.js फ़ाइल को इस पूरे कोड से अपडेट करें. इस वर्शन में, नई handleResultClick विधि शामिल है. साथ ही, attachEventListeners और performSearch में अपडेट किया गया लॉजिक शामिल है.

// 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();
});

अपने काम की जांच करना

अपनी script.js फ़ाइल सेव करें और पेज को रीफ़्रेश करें. अब ऐप्लिकेशन में कई इंटरैक्टिव सुविधाएं होनी चाहिए.

इनकी पुष्टि करें:

  • खोज बॉक्स में "Enter" दबाकर खोजने की सुविधा काम करती है.
  • किसी भी फ़िल्टर (कीमत, रेटिंग, अभी खुला है) को बदलने पर, नई खोज शुरू हो जाती है और नतीजे अपडेट हो जाते हैं.
  • साइडबार में मौजूद नतीजों की सूची में किसी आइटम पर क्लिक करने से, अब मैप उस आइटम की जगह पर आसानी से पैन हो जाता है.

अगले सेक्शन में, हम जानकारी वाला कार्ड लागू करेंगे. यह कार्ड, मार्कर पर क्लिक करने पर दिखता है.

7. जगह के बारे में ज़्यादा जानकारी देने वाले एलिमेंट को लागू करना

हमारा ऐप्लिकेशन अब पूरी तरह से इंटरैक्टिव है. हालांकि, इसमें एक ज़रूरी सुविधा मौजूद नहीं है: चुनी गई जगह के बारे में ज़्यादा जानकारी देखने की सुविधा. इस सेक्शन में, हम 'जगह की जानकारी' एलिमेंट को लागू करेंगे. यह तब दिखेगा, जब कोई उपयोगकर्ता मैप पर मौजूद किसी मार्कर पर क्लिक करेगा या 'जगह की खोज' एलिमेंट में मौजूद किसी आइटम को चुनेगा.

ज़्यादा जानकारी वाले कार्ड का ऐसा कंटेनर बनाना जिसे फिर से इस्तेमाल किया जा सके

मैप पर किसी जगह की जानकारी दिखाने का सबसे आसान तरीका यह है कि एक ऐसा कंटेनर बनाया जाए जिसे बार-बार इस्तेमाल किया जा सके. हम इस कंटेनर के तौर पर AdvancedMarkerElement का इस्तेमाल करेंगे. इसका कॉन्टेंट, छिपा हुआ gmp-place-details-compact विजेट होगा. यह विजेट, हमारे index.html में पहले से मौजूद है.

इस रीयूज़ेबल मार्कर को बनाने के लिए, initDetailsPopup नाम का नया तरीका इस्तेमाल किया जाएगा. यह ऐप्लिकेशन लोड होने पर एक बार बनाया जाएगा और यह छिपा हुआ होगा. हम इस तरीके में मुख्य मैप में एक लिसनर भी जोड़ेंगे, ताकि मैप पर कहीं भी क्लिक करने से जानकारी वाला कार्ड छिप जाए.

मार्कर क्लिक करने के व्यवहार को अपडेट करना

इसके बाद, हमें यह अपडेट करना होगा कि जब कोई उपयोगकर्ता किसी जगह के मार्कर पर क्लिक करता है, तो क्या होता है. अब 'click' तरीके के अंदर मौजूद 'click' लिसनर, जानकारी वाला कार्ड दिखाने के लिए ज़िम्मेदार होगा.addMarkers

किसी मार्कर पर क्लिक करने पर, लिसनर ये काम करेगा:

  1. मैप को मार्कर की जगह पर ले जाएं.
  2. उस जगह की जानकारी के साथ, जानकारी वाले कार्ड को अपडेट करें.
  3. जानकारी वाले कार्ड को मार्कर की जगह पर रखें और उसे दिखाएं.

सूची में मौजूद किसी आइटम पर क्लिक करने की जानकारी को मार्कर पर क्लिक करने की जानकारी से कनेक्ट करना

आखिर में, हम handleResultClick का तरीका अपडेट करेंगे. अब मैप को सिर्फ़ पैन करने के बजाय, यह प्रोग्राम के हिसाब से उससे जुड़े मार्कर पर click इवेंट को ट्रिगर करेगा. यह एक ऐसा पैटर्न है जिसकी मदद से, हम दोनों इंटरैक्शन के लिए एक ही लॉजिक का दोबारा इस्तेमाल कर सकते हैं. इससे हमारा कोड व्यवस्थित रहता है और उसे बनाए रखना आसान होता है.

JavaScript फ़ाइल अपडेट करना

नीचे दिए गए कोड का इस्तेमाल करके, अपनी script.js फ़ाइल को अपडेट करें. नए या बदले गए सेक्शन, initDetailsPopup तरीका और अपडेट किए गए addMarkers और handleResultClick तरीके हैं.

// 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();
});

अपने काम की जांच करना

अपनी script.js फ़ाइल सेव करें और पेज को रीफ़्रेश करें. अब ऐप्लिकेशन में, मांग पर जानकारी दिखनी चाहिए.

इनकी पुष्टि करें:

  • अब मैप पर किसी मार्कर पर क्लिक करने से, मैप उस मार्कर के हिसाब से सेट हो जाता है. साथ ही, मार्कर के ऊपर स्टाइल किया गया ज़्यादा जानकारी देने वाला कार्ड खुल जाता है.
  • साइडबार में मौजूद नतीजों की सूची में किसी आइटम पर क्लिक करने से भी यही काम होता है.
  • कार्ड से दूर मैप पर क्लिक करने से, कार्ड बंद हो जाता है.
  • नई खोज शुरू करने पर, खुला हुआ कोई भी जानकारी वाला कार्ड बंद हो जाता है.

8. आखिरी में कुछ ज़रूरी बातें जोड़ो

हमारा ऐप्लिकेशन अब पूरी तरह से काम कर रहा है. हालांकि, हम इसमें कुछ और बदलाव कर सकते हैं, ताकि उपयोगकर्ता अनुभव को और बेहतर बनाया जा सके. इस आखिरी सेक्शन में, हम दो मुख्य सुविधाएं लागू करेंगे: एक डाइनैमिक हेडर, जो खोज के नतीजों के बारे में बेहतर जानकारी देता है. साथ ही, उपयोगकर्ता की खोज क्वेरी के लिए अपने-आप फ़ॉर्मैट होने की सुविधा.

डाइनैमिक नतीजों का हेडर बनाना

फ़िलहाल, साइडबार के हेडर में हमेशा "नतीजे" लिखा होता है. हम इसे ज़्यादा जानकारी वाला बना सकते हैं. इसके लिए, हमें इसे मौजूदा खोज के हिसाब से अपडेट करना होगा. उदाहरण के लिए, "न्यूयॉर्क के आस-पास बर्गर."

इसके लिए, हम Geocoding API का इस्तेमाल करेंगे. इससे मैप के सेंटर के निर्देशांकों को ऐसी जगह में बदला जा सकेगा जिसे आसानी से समझा जा सके. जैसे, शहर का नाम. इस लॉजिक को मैनेज करने के लिए, async का नया तरीका, updateResultsHeader इस्तेमाल किया जाएगा. जब भी कोई खोज की जाएगी, तब इसे कॉल किया जाएगा.

उपयोगकर्ता की खोज क्वेरी को फ़ॉर्मैट करना

हम उपयोगकर्ता के खोज शब्द को अपने-आप "टाइटल केस" में फ़ॉर्मैट कर देंगे.इससे यह पक्का किया जा सकेगा कि यूज़र इंटरफ़ेस (यूआई) साफ़-सुथरा और एक जैसा दिखे. उदाहरण के लिए, "burger restaurant" को "Burger Restaurant" में बदल दिया जाता है). इस ट्रांसफ़ॉर्मेशन को एक हेल्पर फ़ंक्शन, toTitleCase, मैनेज करेगा. performSearch तरीके को अपडेट किया जाएगा, ताकि खोज करने और हेडर को अपडेट करने से पहले, उपयोगकर्ता के इनपुट पर इस फ़ंक्शन का इस्तेमाल किया जा सके.

JavaScript फ़ाइल अपडेट करना

कोड के फ़ाइनल वर्शन के साथ अपनी script.js फ़ाइल अपडेट करें. इसमें नए toTitleCase और updateResultsHeader तरीके शामिल हैं. साथ ही, अपडेट किया गया performSearch तरीका भी शामिल है, जिसमें इन दोनों को इंटिग्रेट किया गया है.

// 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();
});

अपने काम की जांच करना

अपनी script.js फ़ाइल सेव करें और पेज को रीफ़्रेश करें.

इन सुविधाओं की पुष्टि करें:

  • खोज बॉक्स में pizza (सभी छोटे अक्षर) टाइप करें और खोजें पर क्लिक करें. बॉक्स में मौजूद टेक्स्ट बदलकर "Pizza" हो जाएगा. साथ ही, साइडबार में मौजूद हेडर अपडेट होकर "New York के आस-पास पिज़्ज़ा" हो जाएगा.
  • मैप को किसी दूसरे शहर, जैसे कि बॉस्टन पर पैन करें और फिर से खोजें. हेडर अपडेट होकर "बोस्टन के आस-पास पिज़्ज़ा" दिखना चाहिए.

9. बधाई हो

आपने एक ऐसा इंटरैक्टिव लोकल सर्च ऐप्लिकेशन बना लिया है जो Google Maps Platform के JavaScript API की सुविधाओं के साथ-साथ, Places UI Kit को भी आसानी से इस्तेमाल करने की सुविधा देता है.

आपने क्या सीखा

  • स्टेट और लॉजिक को मैनेज करने के लिए, JavaScript क्लास का इस्तेमाल करके मैपिंग ऐप्लिकेशन को कैसे स्ट्रक्चर करें.
  • तेज़ी से यूज़र इंटरफ़ेस (यूआई) डेवलप करने के लिए, Google Maps JavaScript API के साथ Places UI Kit का इस्तेमाल करने का तरीका.
  • मैप पर लोकप्रिय जगहों को दिखाने के लिए, प्रोग्राम के हिसाब से ऐडवांस मार्कर जोड़ने और मैनेज करने का तरीका.
  • उपयोगकर्ता को बेहतर अनुभव देने के लिए, जियोकोडिंग सेवा का इस्तेमाल करके, निर्देशांकों को ऐसे पतों में बदलने का तरीका जिन्हें आसानी से पढ़ा जा सकता है.
  • स्टेट फ़्लैग का इस्तेमाल करके, इंटरैक्टिव ऐप्लिकेशन में रेस कंडीशन की पहचान करने और उन्हें ठीक करने का तरीका. साथ ही, यह पक्का करना कि कॉम्पोनेंट की प्रॉपर्टी सही तरीके से अपडेट की गई हों.

आगे क्या करना है?

आपको और कौनसे कोडलैब देखने हैं?

मैप पर डेटा विज़ुअलाइज़ेशन अपने मैप की स्टाइल को पसंद के मुताबिक बनाने के बारे में ज़्यादा जानकारी मैप में 3D इंटरैक्शन के लिए बिल्डिंग

क्या आपको अपनी पसंद का कोडलैब नहीं मिल रहा है? यहां नई समस्या के साथ इसका अनुरोध करें.