Places UI কিট দিয়ে একটি স্থানীয় অনুসন্ধান অ্যাপ তৈরি করুন

1. আপনি শুরু করার আগে

এই কোডল্যাব আপনাকে শেখায় কিভাবে Google Maps Platform Places UI Kit ব্যবহার করে একটি সম্পূর্ণ ইন্টারেক্টিভ স্থানীয় অনুসন্ধান অ্যাপ্লিকেশন তৈরি করতে হয়।

সমাপ্ত প্লেসফাইন্ডার অ্যাপ্লিকেশনটির একটি স্ক্রিনশট, মার্কার সহ নিউইয়র্কের একটি মানচিত্র, অনুসন্ধান ফলাফল সহ একটি সাইডবার এবং একটি বিশদ কার্ডোপেন দেখানো হয়েছে৷

পূর্বশর্ত

  • প্রয়োজনীয় API এবং শংসাপত্রগুলি কনফিগার করা একটি Google ক্লাউড প্রকল্প৷
  • HTML এবং CSS এর প্রাথমিক জ্ঞান।
  • আধুনিক জাভাস্ক্রিপ্ট বোঝা।
  • একটি আধুনিক ওয়েব ব্রাউজার, যেমন Chrome এর সর্বশেষ সংস্করণ।
  • আপনার পছন্দের একটি পাঠ্য সম্পাদক।

আপনি কি করবেন

  • একটি জাভাস্ক্রিপ্ট ক্লাস ব্যবহার করে একটি ম্যাপিং অ্যাপ্লিকেশন গঠন করুন।
  • একটি মানচিত্র প্রদর্শন করতে ওয়েব উপাদান ব্যবহার করুন
  • একটি পাঠ্য অনুসন্ধানের ফলাফল সম্পাদন এবং প্রদর্শন করতে স্থান অনুসন্ধান উপাদান ব্যবহার করুন৷
  • প্রোগ্রাম্যাটিকভাবে কাস্টম AdvancedMarkerElement ম্যাপ মার্কার তৈরি এবং পরিচালনা করুন।
  • যখন একজন ব্যবহারকারী একটি অবস্থান নির্বাচন করে তখন স্থানের বিবরণের উপাদান প্রদর্শন করুন।
  • একটি গতিশীল এবং ব্যবহারকারী-বান্ধব ইন্টারফেস তৈরি করতে জিওকোডিং API ব্যবহার করুন।

আপনি কি প্রয়োজন হবে

  • বিলিং সক্ষম সহ একটি Google ক্লাউড প্রকল্প৷
  • একটি Google মানচিত্র প্ল্যাটফর্ম API কী
  • একটি মানচিত্র আইডি
  • নিম্নলিখিত API সক্রিয় করা হয়েছে:
    • মানচিত্র জাভাস্ক্রিপ্ট API
    • স্থান UI কিট
    • জিওকোডিং API

2. সেট আপ করুন

নিম্নলিখিত সক্রিয়করণ পদক্ষেপের জন্য, আপনাকে মানচিত্র জাভাস্ক্রিপ্ট API, স্থান UI কিট এবং জিওকোডিং API সক্ষম করতে হবে।

Google Maps প্ল্যাটফর্ম সেট আপ করুন

আপনার যদি ইতিমধ্যেই একটি Google ক্লাউড প্ল্যাটফর্ম অ্যাকাউন্ট না থাকে এবং বিলিং সক্ষম করা একটি প্রকল্প থাকে, তাহলে অনুগ্রহ করে একটি বিলিং অ্যাকাউন্ট এবং একটি প্রকল্প তৈরি করতে Google মানচিত্র প্ল্যাটফর্মের সাথে শুরু করা নির্দেশিকাটি দেখুন৷

  1. ক্লাউড কনসোলে , প্রকল্পের ড্রপ-ডাউন মেনুতে ক্লিক করুন এবং এই কোডল্যাবের জন্য আপনি যে প্রকল্পটি ব্যবহার করতে চান সেটি নির্বাচন করুন।

  1. Google ক্লাউড মার্কেটপ্লেসে এই কোডল্যাবের জন্য প্রয়োজনীয় Google মানচিত্র প্ল্যাটফর্ম API এবং SDK সক্ষম করুন৷ এটি করতে, এই ভিডিও বা এই ডকুমেন্টেশনের ধাপগুলি অনুসরণ করুন৷
  2. ক্লাউড কনসোলের শংসাপত্র পৃষ্ঠায় একটি API কী তৈরি করুন। আপনি এই ভিডিও বা এই ডকুমেন্টেশনের ধাপগুলি অনুসরণ করতে পারেন। Google মানচিত্র প্ল্যাটফর্মের সমস্ত অনুরোধের জন্য একটি API কী প্রয়োজন৷

3. অ্যাপ্লিকেশন শেল এবং একটি কার্যকরী মানচিত্র

এই প্রথম ধাপে, আমরা আমাদের অ্যাপ্লিকেশনের জন্য সম্পূর্ণ ভিজ্যুয়াল লেআউট তৈরি করব এবং আমাদের জাভাস্ক্রিপ্টের জন্য একটি পরিষ্কার, শ্রেণি-ভিত্তিক কাঠামো স্থাপন করব। এটি আমাদের গড়ে তোলার জন্য একটি শক্ত ভিত্তি দেয়। এই বিভাগের শেষ নাগাদ, আপনার কাছে একটি স্টাইলযুক্ত পৃষ্ঠা থাকবে যা একটি ইন্টারেক্টিভ মানচিত্র প্রদর্শন করবে।

HTML ফাইল তৈরি করুন

প্রথমে, index.html নামে একটি ফাইল তৈরি করুন। এই ফাইলটিতে হেডার, সার্চ ফিল্টার, সাইডবার, ম্যাপ কন্টেইনার এবং প্রয়োজনীয় ওয়েব উপাদান সহ আমাদের অ্যাপ্লিকেশনের সম্পূর্ণ কাঠামো থাকবে।

নিচের কোডটি index.html এ কপি করুন। YOUR_API_KEY_HERE আপনার নিজের Google Maps Platform API কী দিয়ে এবং DEMO_MAP_ID আপনার নিজের Google মানচিত্র প্ল্যাটফর্ম মানচিত্র ID দিয়ে প্রতিস্থাপন করতে ভুলবেন না।

<!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 ফাইল তৈরি করুন

এরপর, style.css নামে একটি ফাইল তৈরি করুন। শুরু থেকে একটি পরিষ্কার, আধুনিক চেহারা প্রতিষ্ঠা করতে আমরা এখনই প্রয়োজনীয় সমস্ত স্টাইলিং যোগ করব। এই CSS সামগ্রিক বিন্যাস, রঙ, ফন্ট এবং আমাদের সমস্ত UI উপাদানগুলির উপস্থিতি পরিচালনা করে।

নিচের কোডটি 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);
}

জাভাস্ক্রিপ্ট অ্যাপ্লিকেশন ক্লাস তৈরি করুন

অবশেষে, script.js নামে একটি ফাইল তৈরি করুন। আমরা PlaceFinderApp নামক একটি জাভাস্ক্রিপ্ট ক্লাসের মধ্যে আমাদের অ্যাপ্লিকেশন গঠন করব। এটি আমাদের কোড সংগঠিত রাখে এবং রাষ্ট্রকে পরিষ্কারভাবে পরিচালনা করে।

এই প্রাথমিক কোডটি ক্লাস সংজ্ঞায়িত করবে, constructor আমাদের সমস্ত HTML উপাদানগুলি খুঁজে পাবে এবং Google মানচিত্র প্ল্যাটফর্ম লাইব্রেরিগুলি লোড করার জন্য একটি 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();
});

API কী সীমাবদ্ধতা

এই কোডল্যাব কাজ করার জন্য আপনাকে আপনার API কীতে একটি নতুন সীমাবদ্ধতা যোগ করতে হতে পারে। আরও তথ্যের জন্য আপনার এপিআই কীগুলিকে সীমাবদ্ধ করুন এবং এটি কীভাবে করবেন সে সম্পর্কে নির্দেশিকা দেখুন৷

আপনার কাজ পরীক্ষা করুন

আপনার ওয়েব ব্রাউজারে index.html ফাইলটি খুলুন। আপনি একটি শিরোনাম সহ একটি পৃষ্ঠা দেখতে পাবেন যেখানে একটি অনুসন্ধান বার এবং ফিল্টার রয়েছে, "আপনার অনুসন্ধানের ফলাফল এখানে প্রদর্শিত হবে" বার্তা সহ একটি সাইডবার এবং নিউ ইয়র্ক সিটিকে কেন্দ্র করে একটি বড় মানচিত্র দেখতে হবে৷ এই পর্যায়ে, অনুসন্ধান নিয়ন্ত্রণগুলি এখনও কার্যকর নয়৷

4. একটি অনুসন্ধান ফাংশন বাস্তবায়ন

এই বিভাগে, আমরা মূল অনুসন্ধান কার্যকারিতা বাস্তবায়নের মাধ্যমে আমাদের অ্যাপ্লিকেশনটিকে প্রাণবন্ত করব। আমরা কোড লিখব যেটি চলে যখন একজন ব্যবহারকারী "অনুসন্ধান" বোতামে ক্লিক করে। ব্যবহারকারীর ইন্টারঅ্যাকশনগুলিকে সুন্দরভাবে পরিচালনা করতে এবং রেসের অবস্থার মতো সাধারণ বাগগুলি প্রতিরোধ করতে আমরা শুরু থেকেই সেরা অনুশীলনের সাথে এই ফাংশনটি তৈরি করব।

এই ধাপের শেষে, আপনি অনুসন্ধান বোতামে ক্লিক করতে সক্ষম হবেন এবং অ্যাপ্লিকেশনটি ব্যাকগ্রাউন্ডে ডেটা আনার সময় একটি লোডিং স্পিনার প্রদর্শিত হবে।

অনুসন্ধান পদ্ধতি তৈরি করুন

প্রথমে, আমাদের PlaceFinderApp ক্লাসের মধ্যে performSearch পদ্ধতি সংজ্ঞায়িত করুন। এই ফাংশন আমাদের অনুসন্ধান যুক্তির হৃদয় হবে. "দারোয়ান" হিসাবে কাজ করার জন্য আমরা একটি ইনস্ট্যান্স ভেরিয়েবল, isSearchInProgress প্রবর্তন করব। এটি ব্যবহারকারীকে একটি নতুন অনুসন্ধান শুরু করতে বাধা দেয় যখন একটি ইতিমধ্যেই চলছে, যা ত্রুটির কারণ হতে পারে৷

performSearch মধ্যে যুক্তি জটিল মনে হতে পারে, তাই আমরা এটি ভেঙে দেব:

  1. এটি প্রথমে পরীক্ষা করে যে একটি অনুসন্ধান ইতিমধ্যেই চলছে কিনা। যদি তাই হয়, এটি কিছুই করে না।
  2. এটি ফাংশনটিকে "লক" করতে isSearchInProgress পতাকাকে true সেট করে।
  3. এটি লোডিং স্পিনার দেখায় এবং নতুন ফলাফলের জন্য UI প্রস্তুত করে।
  4. এটি অনুসন্ধান অনুরোধের textQuery বৈশিষ্ট্য null এ সেট করে। এটি একটি গুরুত্বপূর্ণ পদক্ষেপ যা ওয়েব কম্পোনেন্টকে স্বীকার করতে বাধ্য করে যে একটি নতুন অনুরোধ আসছে।
  5. এটি 0 বিলম্বের সাথে একটি setTimeout ব্যবহার করে। এই স্ট্যান্ডার্ড জাভাস্ক্রিপ্ট কৌশলটি পরবর্তী ব্রাউজার টাস্কে চালানোর জন্য আমাদের বাকি কোডের সময়সূচী নির্ধারণ করে, নিশ্চিত করে যে উপাদানটি প্রথমে null মান প্রক্রিয়া করেছে। এমনকি যদি ব্যবহারকারী একই জিনিস দুইবার অনুসন্ধান করে, একটি নতুন অনুসন্ধান সর্বদা ট্রিগার হবে।

ইভেন্ট শ্রোতা যোগ করুন

এরপরে, ব্যবহারকারী অ্যাপের সাথে ইন্টারঅ্যাক্ট করলে আমাদের performSearch পদ্ধতিতে কল করতে হবে। আমাদের সমস্ত ইভেন্ট-হ্যান্ডলিং কোড এক জায়গায় রাখতে আমরা একটি নতুন পদ্ধতি তৈরি করব, attachEventListeners । আপাতত, আমরা শুধু অনুসন্ধান বোতামের click ইভেন্টের জন্য একজন শ্রোতা যোগ করব। আমরা আরেকটি ইভেন্টের জন্য একটি স্থানধারক যোগ করব, gmp-load , যা আমরা পরবর্তী ধাপে ব্যবহার করব।

জাভাস্ক্রিপ্ট ফাইল আপডেট করুন

নিম্নলিখিত কোড দিয়ে আপনার 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. ফলাফল প্রদর্শন করুন এবং মার্কার যোগ করুন

এখন যেহেতু অনুসন্ধান ট্রিগার কার্যকরী, পরবর্তী কাজটি হল স্ক্রিনে ফলাফলগুলি প্রদর্শন করা। এই বিভাগের কোডটি সার্চ লজিককে UI এর সাথে সংযুক্ত করবে। একবার স্থান অনুসন্ধান উপাদান ডেটা লোড করা শেষ করে, এটি অনুসন্ধান "লক" প্রকাশ করবে, লোডিং স্পিনার লুকাবে এবং প্রতিটি ফলাফলের জন্য মানচিত্রে একটি মার্কার প্রদর্শন করবে৷

অনুসন্ধান সমাপ্তির জন্য শুনুন

স্থান অনুসন্ধান উপাদানটি একটি gmp-load ইভেন্ট চালু করে যখন এটি সফলভাবে ডেটা নিয়ে আসে। ফলাফলগুলি প্রক্রিয়া করার জন্য এটি আমাদের জন্য নিখুঁত সংকেত।

প্রথমে, আমাদের attachEventListeners পদ্ধতিতে এই ইভেন্টের জন্য একটি ইভেন্ট লিসেনার যোগ করুন।

মার্কার-হ্যান্ডলিং পদ্ধতি তৈরি করুন

এর পরে, আমরা দুটি নতুন সহায়ক পদ্ধতি তৈরি করব: clearMarkers এবং addMarkers

  • clearMarkers() পূর্ববর্তী অনুসন্ধান থেকে যেকোনো মার্কার মুছে ফেলবে।
  • addMarkers() কে আমাদের gmp-load শ্রোতা কল করবে। এটি অনুসন্ধান দ্বারা প্রত্যাবর্তিত স্থানগুলির তালিকার মধ্য দিয়ে লুপ করবে এবং প্রতিটির জন্য একটি নতুন AdvancedMarkerElement তৈরি করবে৷ এখানেও আমরা লোডিং স্পিনার লুকিয়ে রাখব এবং অনুসন্ধান চক্রটি সম্পূর্ণ করে isSearchInProgress লক প্রকাশ করব।

লক্ষ্য করুন যে আমরা প্লেস আইডিটিকে কী হিসাবে ব্যবহার করে একটি বস্তুতে ( this.markers ) মার্কার সংরক্ষণ করছি। এটি মার্কারগুলি পরিচালনা করার একটি উপায় এবং পরবর্তীতে একটি নির্দিষ্ট মার্কার খুঁজে পেতে আমাদের অনুমতি দেবে৷

অবশেষে, আমাদের প্রতিটি নতুন অনুসন্ধানের শুরুতে clearMarkers() কল করতে হবে। এর জন্য সেরা জায়গা হল performSearch ভিতরে।

জাভাস্ক্রিপ্ট ফাইল আপডেট করুন

আপনার 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. অনুসন্ধান ফিল্টার এবং তালিকা ইন্টারঅ্যাক্টিভিটি সক্রিয় করুন

আমাদের অ্যাপ্লিকেশন এখন অনুসন্ধান ফলাফল প্রদর্শন করতে পারে, কিন্তু এটি এখনও ইন্টারেক্টিভ নয়. এই বিভাগে, আমরা সমস্ত ব্যবহারকারীর নিয়ন্ত্রণকে জীবন্ত করে তুলব। আমরা ফিল্টারগুলি সক্রিয় করব, "এন্টার" কী দিয়ে অনুসন্ধান সক্ষম করব এবং ফলাফলের তালিকায় থাকা আইটেমগুলিকে মানচিত্রে তাদের সংশ্লিষ্ট অবস্থানগুলির সাথে সংযুক্ত করব৷

এই ধাপের শেষে, অ্যাপ্লিকেশনটি ব্যবহারকারীর ইনপুটের প্রতি সম্পূর্ণ প্রতিক্রিয়াশীল বোধ করবে।

অনুসন্ধান ফিল্টার সক্রিয় করুন

প্রথমত, হেডারের সমস্ত ফিল্টার নিয়ন্ত্রণ থেকে মানগুলি পড়ার জন্য performSearch পদ্ধতি আপডেট করা হবে। প্রতিটি ফিল্টারের জন্য (মূল্য, রেটিং, এবং "এখন খুলুন"), অনুসন্ধান চালানোর আগে সংশ্লিষ্ট সম্পত্তি searchRequest অবজেক্টে সেট করা হবে।

সমস্ত নিয়ন্ত্রণের জন্য ইভেন্ট শ্রোতা যোগ করুন

এর পরে, আমরা আমাদের attachEventListeners পদ্ধতি প্রসারিত করব। আমরা প্রতিটি ফিল্টার নিয়ন্ত্রণে change ইভেন্টের জন্য শ্রোতাদের যোগ করব, সেইসাথে অনুসন্ধান ইনপুটে একটি keydown শ্রোতা যোগ করব যাতে ব্যবহারকারী যখন "এন্টার" কী টিপেন তখন সনাক্ত করতে। এই সমস্ত নতুন শ্রোতারা performSearch পদ্ধতিকে কল করবে।

ফলাফলের তালিকাকে মানচিত্রের সাথে সংযুক্ত করুন

একটি নির্বিঘ্ন অভিজ্ঞতা তৈরি করতে, সাইডবার ফলাফল তালিকায় একটি আইটেম ক্লিক করে সেই অবস্থানে মানচিত্র ফোকাস করা উচিত।

একটি নতুন পদ্ধতি, handleResultClick , gmp-select ইভেন্টের জন্য শুনবে, যা একটি আইটেম ক্লিক করার সময় স্থান অনুসন্ধান উপাদান দ্বারা বহিস্কার করা হয়। এই ফাংশনটি সংশ্লিষ্ট স্থানের অবস্থান খুঁজে বের করবে এবং এতে মানচিত্রটি মসৃণভাবে প্যান করবে।

এটি কাজ করার জন্য, নিশ্চিত করুন যে selectable বৈশিষ্ট্যটি index.html এ আপনার gmp-place-search উপাদানটিতে উপস্থিত রয়েছে।

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

জাভাস্ক্রিপ্ট ফাইল আপডেট করুন

নিম্নলিখিত সম্পূর্ণ কোড দিয়ে আপনার 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 ফাইলটি সংরক্ষণ করুন এবং পৃষ্ঠাটি রিফ্রেশ করুন৷ অ্যাপ্লিকেশন এখন অত্যন্ত ইন্টারেক্টিভ হওয়া উচিত.

নিম্নলিখিত যাচাই করুন:

  • অনুসন্ধান বাক্সে "এন্টার" টিপে অনুসন্ধান করা কাজ করে।
  • যে কোনো ফিল্টার (মূল্য, রেটিং, এখন খুলুন) পরিবর্তন করা একটি নতুন অনুসন্ধান শুরু করে এবং ফলাফল আপডেট করে।
  • সাইডবার ফলাফল তালিকায় একটি আইটেম ক্লিক করা এখন মসৃণভাবে মানচিত্রটিকে সেই আইটেমের অবস্থানে প্যান করে।

পরবর্তী বিভাগে, আমরা বিশদ কার্ড প্রয়োগ করব যা একটি মার্কার ক্লিক করা হলে প্রদর্শিত হয়।

7. স্থানের বিশদ উপাদান প্রয়োগ করুন

আমাদের অ্যাপ্লিকেশন এখন সম্পূর্ণ ইন্টারেক্টিভ, কিন্তু এটি একটি মূল বৈশিষ্ট্য অনুপস্থিত: একটি নির্বাচিত স্থান সম্পর্কে আরও তথ্য দেখার ক্ষমতা. এই বিভাগে, আমরা স্থানের বিশদ উপাদানটি প্রয়োগ করব যা প্রদর্শিত হবে যখন একজন ব্যবহারকারী মানচিত্রে একটি মার্কার ক্লিক করবে, বা স্থান অনুসন্ধান উপাদানে একটি আইটেম নির্বাচন করবে।

একটি পুনরায় ব্যবহারযোগ্য বিবরণ কার্ড ধারক তৈরি করুন

মানচিত্রে স্থানের বিবরণ প্রদর্শনের সবচেয়ে কার্যকর উপায় হল একটি একক, পুনঃব্যবহারযোগ্য ধারক তৈরি করা। আমরা এই ধারক হিসাবে একটি AdvancedMarkerElement ব্যবহার করব। এর বিষয়বস্তু হবে লুকানো gmp-place-details-compact উইজেট যা আমাদের index.html এ ইতিমধ্যেই রয়েছে।

একটি নতুন পদ্ধতি, initDetailsPopup , এই পুনঃব্যবহারযোগ্য মার্কার তৈরির কাজ পরিচালনা করবে। এটি একবার তৈরি করা হবে যখন অ্যাপ্লিকেশন লোড হবে এবং লুকানো শুরু হবে। আমরা এই পদ্ধতিতে মূল মানচিত্রে একজন শ্রোতাকেও যুক্ত করব, যাতে মানচিত্রের যে কোনও জায়গায় ক্লিক করলে বিশদ কার্ডটি লুকিয়ে যায়।

মার্কার ক্লিক আচরণ আপডেট করুন

এরপরে, একজন ব্যবহারকারী একটি স্থান চিহ্নিতকারীতে ক্লিক করলে কী ঘটে তা আমাদের আপডেট করতে হবে। addMarkers পদ্ধতির ভিতরে 'click' শ্রোতা এখন বিশদ কার্ড দেখানোর জন্য দায়ী থাকবে।

যখন একটি মার্কার ক্লিক করা হয়, শ্রোতা করবে:

  1. চিহ্নিতকারীর অবস্থানে মানচিত্রটি প্যান করুন।
  2. সেই নির্দিষ্ট স্থানের তথ্য সহ বিশদ কার্ড আপডেট করুন।
  3. বিশদ কার্ডটি চিহ্নিতকারীর অবস্থানে রাখুন এবং এটি দৃশ্যমান করুন।

মার্কার ক্লিকের সাথে তালিকা ক্লিক সংযুক্ত করুন

অবশেষে, আমরা handleResultClick পদ্ধতি আপডেট করব। শুধু মানচিত্র প্যান করার পরিবর্তে, এটি এখন প্রোগ্রামেটিকভাবে সংশ্লিষ্ট মার্কারে click ইভেন্টটিকে ট্রিগার করবে। এটি একটি শক্তিশালী প্যাটার্ন যা আমাদের কোডকে পরিষ্কার এবং রক্ষণাবেক্ষণযোগ্য রেখে উভয় ইন্টারঅ্যাকশনের জন্য একই যুক্তি পুনরায় ব্যবহার করতে দেয়।

জাভাস্ক্রিপ্ট ফাইল আপডেট করুন

নিম্নলিখিত কোড দিয়ে আপনার 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. চূড়ান্ত পলিশ যোগ করুন

আমাদের অ্যাপ্লিকেশন এখন সম্পূর্ণরূপে কার্যকরী, কিন্তু ব্যবহারকারীর অভিজ্ঞতাকে আরও ভালো করার জন্য আমরা কিছু চূড়ান্ত স্পর্শ যোগ করতে পারি। এই চূড়ান্ত বিভাগে, আমরা দুটি মূল বৈশিষ্ট্য বাস্তবায়ন করব: একটি গতিশীল শিরোনাম যা অনুসন্ধান ফলাফলের জন্য আরও ভাল প্রসঙ্গ প্রদান করে এবং ব্যবহারকারীর অনুসন্ধান প্রশ্নের জন্য স্বয়ংক্রিয় বিন্যাস।

একটি গতিশীল ফলাফল শিরোনাম তৈরি করুন

এই মুহুর্তে, সাইডবার হেডার সবসময় বলে "ফলাফল।" বর্তমান অনুসন্ধান প্রতিফলিত করার জন্য এটিকে আপডেট করে আমরা এটিকে আরও তথ্যপূর্ণ করতে পারি। উদাহরণস্বরূপ, "নিউ ইয়র্কের কাছাকাছি বার্গার।"

এটি করার জন্য, আমরা জিওকোডিং API ব্যবহার করব মানচিত্রের কেন্দ্র স্থানাঙ্কগুলিকে একটি শহরের নামের মতো মানুষের-পাঠযোগ্য স্থানে রূপান্তর করতে। একটি নতুন async পদ্ধতি, updateResultsHeader , এই যুক্তিটি পরিচালনা করবে। প্রতিবার অনুসন্ধান করা হলে এটি বলা হবে।

ব্যবহারকারীর অনুসন্ধান ক্যোয়ারী বিন্যাস

UI পরিষ্কার এবং সামঞ্জস্যপূর্ণ দেখায় তা নিশ্চিত করতে, আমরা স্বয়ংক্রিয়ভাবে ব্যবহারকারীর অনুসন্ধান শব্দটিকে "টাইটেল কেস" এ ফর্ম্যাট করব (যেমন, "বার্গার রেস্তোরাঁ" "বার্গার রেস্টুরেন্ট" হয়ে যায়)। একটি সহায়ক ফাংশন, toTitleCase , এই রূপান্তরটি পরিচালনা করবে। সার্চ করার আগে এবং হেডার আপডেট করার আগে ব্যবহারকারীর ইনপুটে এই ফাংশনটি ব্যবহার করার জন্য performSearch পদ্ধতি আপডেট করা হবে।

জাভাস্ক্রিপ্ট ফাইল আপডেট করুন

কোডের চূড়ান্ত সংস্করণ দিয়ে আপনার 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 (সমস্ত ছোট হাতের) টাইপ করুন এবং অনুসন্ধানে ক্লিক করুন। বাক্সের পাঠ্যটি "পিজ্জা"-তে পরিবর্তিত হওয়া উচিত এবং সাইডবারে হেডারটি "নিউ ইয়র্কের কাছে পিজা" বলে আপডেট হওয়া উচিত।
  • বোস্টনের মতো একটি ভিন্ন শহরে মানচিত্র প্যান করুন এবং আবার অনুসন্ধান করুন৷ শিরোনামটি "বোস্টনের কাছে পিজা"-তে আপডেট হওয়া উচিত।

9. অভিনন্দন

আপনি সফলভাবে একটি সম্পূর্ণ, ইন্টারেক্টিভ স্থানীয় অনুসন্ধান অ্যাপ্লিকেশন তৈরি করেছেন যা মূল Google মানচিত্র প্ল্যাটফর্ম জাভাস্ক্রিপ্ট API-এর শক্তির সাথে স্থান UI কিটের সরলতাকে একত্রিত করে।

আপনি যা শিখেছেন

  • রাষ্ট্র এবং যুক্তি পরিচালনা করতে একটি জাভাস্ক্রিপ্ট ক্লাস ব্যবহার করে একটি ম্যাপিং অ্যাপ্লিকেশন কীভাবে গঠন করবেন।
  • দ্রুত UI ডেভেলপমেন্টের জন্য Google Maps JavaScript API-এর সাথে Places UI কিট কীভাবে ব্যবহার করবেন।
  • ম্যাপে আগ্রহের কাস্টম পয়েন্টগুলি প্রদর্শন করতে কীভাবে প্রোগ্রাম্যাটিকভাবে অ্যাডভান্সড মার্কারগুলিকে যুক্ত এবং পরিচালনা করবেন।
  • একটি ভাল ব্যবহারকারীর অভিজ্ঞতার জন্য স্থানাঙ্কগুলিকে মানব-পাঠযোগ্য ঠিকানায় পরিণত করতে জিওকোডিং পরিষেবা কীভাবে ব্যবহার করবেন।
  • রাষ্ট্রীয় পতাকা ব্যবহার করে এবং উপাদান বৈশিষ্ট্যগুলি সঠিকভাবে আপডেট করা হয়েছে তা নিশ্চিত করে একটি ইন্টারেক্টিভ অ্যাপ্লিকেশনে সাধারণ রেসের শর্তগুলি কীভাবে সনাক্ত এবং ঠিক করা যায়।

এরপর কি?

আপনি অন্য কোন কোডল্যাব দেখতে চান?

মানচিত্রে ডেটা ভিজ্যুয়ালাইজেশন আমার মানচিত্রের শৈলী কাস্টমাইজ করার বিষয়ে আরও মানচিত্রে 3D মিথস্ক্রিয়া জন্য বিল্ডিং

আপনি সবচেয়ে আগ্রহী কোডল্যাব খুঁজে পাচ্ছেন না? এখানে একটি নতুন সমস্যা সহ এটি অনুরোধ করুন