Skip to content

Instantly share code, notes, and snippets.

@hhkaos
Created February 1, 2022 10:27
Show Gist options
  • Save hhkaos/35f90fec2e5451621d321e75cab1aa9e to your computer and use it in GitHub Desktop.
Save hhkaos/35f90fec2e5451621d321e75cab1aa9e to your computer and use it in GitHub Desktop.
GMaps Platform: Solution Library
<!DOCTYPE html>
<html>
<head>
<title>Address Selection</title>
<style>
body {
margin: 0;
}
.sb-title {
position: relative;
top: -12px;
font-family: Roboto, sans-serif;
font-weight: 500;
}
.sb-title-icon {
position: relative;
top: -5px;
}
.card-container {
display: flex;
height: 500px;
width: 600px;
}
.panel {
background: white;
width: 300px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.half-input-container {
display: flex;
justify-content: space-between;
}
.half-input {
max-width: 120px;
}
.map {
width: 300px;
}
h2 {
margin: 0;
font-family: Roboto, sans-serif;
}
input {
height: 30px;
}
input {
border: 0;
border-bottom: 1px solid black;
font-size: 14px;
font-family: Roboto, sans-serif;
font-style: normal;
font-weight: normal;
}
input:focus::placeholder {
color: white;
}
.button-cta {
height: 40px;
width: 40%;
background: #3367d6;
color: white;
font-size: 15px;
text-transform: uppercase;
font-family: Roboto, sans-serif;
border: 0;
border-radius: 3px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.48);
cursor: pointer;
}
</style>
<script>
"use strict";
function initMap() {
const componentForm = [
'location',
'locality',
'administrative_area_level_1',
'country',
'postal_code',
];
const map = new google.maps.Map(document.getElementById("map"), {
zoom: 11,
center: { lat: 37.4221, lng: -122.0841 },
mapTypeControl: false,
fullscreenControl: true,
zoomControl: true,
streetViewControl: true
});
const marker = new google.maps.Marker({map: map, draggable: false});
const autocompleteInput = document.getElementById('location');
const autocomplete = new google.maps.places.Autocomplete(autocompleteInput, {
fields: ["address_components", "geometry", "name"],
types: ["address"],
});
autocomplete.addListener('place_changed', function () {
marker.setVisible(false);
const place = autocomplete.getPlace();
if (!place.geometry) {
// User entered the name of a Place that was not suggested and
// pressed the Enter key, or the Place Details request failed.
window.alert('No details available for input: \'' + place.name + '\'');
return;
}
renderAddress(place);
fillInAddress(place);
});
function fillInAddress(place) { // optional parameter
const addressNameFormat = {
'street_number': 'short_name',
'route': 'long_name',
'locality': 'long_name',
'administrative_area_level_1': 'short_name',
'country': 'long_name',
'postal_code': 'short_name',
};
const getAddressComp = function (type) {
for (const component of place.address_components) {
if (component.types[0] === type) {
return component[addressNameFormat[type]];
}
}
return '';
};
document.getElementById('location').value = getAddressComp('street_number') + ' '
+ getAddressComp('route');
for (const component of componentForm) {
// Location field is handled separately above as it has different logic.
if (component !== 'location') {
document.getElementById(component).value = getAddressComp(component);
}
}
}
function renderAddress(place) {
map.setCenter(place.geometry.location);
marker.setPosition(place.geometry.location);
marker.setVisible(true);
}
}
</script>
</head>
<body>
<div class="card-container">
<div class="panel">
<div>
<img class="sb-title-icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/location_pin/v5/24px.svg" alt="">
<span class="sb-title">Address Selection</span>
</div>
<input type="text" placeholder="Address" id="location"/>
<input type="text" placeholder="Apt, Suite, etc (optional)"/>
<input type="text" placeholder="City" id="locality"/>
<div class="half-input-container">
<input type="text" class="half-input" placeholder="State/Province" id="administrative_area_level_1"/>
<input type="text" class="half-input" placeholder="Zip/Postal code" id="postal_code"/>
</div>
<input type="text" placeholder="Country" id="country"/>
<button class="button-cta">Checkout</button>
</div>
<div class="map" id="map"></div>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&libraries=places&callback=initMap&solution_channel=GMP_QB_addressselection_v1_cABC" async defer></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Locator</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
<script src="https://ajax.googleapis.com/ajax/libs/handlebars/4.7.7/handlebars.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#map-container {
width: 100%;
height: 100%;
position: relative;
font-family: "Roboto", sans-serif;
box-sizing: border-box;
}
#map-container button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
font-size: inherit;
cursor: pointer;
}
#map {
position: absolute;
left: 22em;
top: 0;
right: 0;
bottom: 0;
}
#locations-panel {
position: absolute;
left: 0;
width: 22em;
top: 0;
bottom: 0;
overflow-y: auto;
background: white;
padding: 0.5em;
box-sizing: border-box;
}
@media only screen and (max-width: 876px) {
#map {
left: 0;
bottom: 50%;
}
#locations-panel {
top: 50%;
right: 0;
width: unset;
}
}
#locations-panel-list > header {
padding: 1.4em 1.4em 0 1.4em;
}
#locations-panel-list h1.search-title {
font-size: 1em;
font-weight: 500;
margin: 0;
}
#locations-panel-list h1.search-title > img {
vertical-align: bottom;
margin-top: -1em;
}
#locations-panel-list .search-input {
width: 100%;
margin-top: 0.8em;
position: relative;
}
#locations-panel-list .search-input input {
width: 100%;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0.3em;
height: 2.2em;
box-sizing: border-box;
padding: 0 2.5em 0 1em;
font-size: 1em;
}
#locations-panel-list .search-input-overlay {
position: absolute;
}
#locations-panel-list .search-input-overlay.search {
right: 2px;
top: 2px;
bottom: 2px;
width: 2.4em;
}
#locations-panel-list .search-input-overlay.search button {
width: 100%;
height: 100%;
border-radius: 0.2em;
color: black;
background: transparent;
}
#locations-panel-list .search-input-overlay.search .icon {
margin-top: 0.05em;
vertical-align: top;
}
#locations-panel-list .section-name {
font-weight: 500;
font-size: 0.9em;
margin: 1.8em 0 1em 1.5em;
}
#locations-panel-list .location-result {
position: relative;
padding: 0.8em 3.5em 0.8em 1.4em;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
cursor: pointer;
}
#locations-panel-list .location-result:first-of-type {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
#locations-panel-list .location-result:last-of-type {
border-bottom: none;
}
#locations-panel-list .location-result.selected {
outline: 2px solid #4285f4;
}
#locations-panel-list button.select-location {
margin-bottom: 0.6em;
text-align: left;
}
#locations-panel-list .location-result h2.name {
font-size: 1em;
font-weight: 500;
margin: 0;
}
#locations-panel-list .location-result .address {
font-size: 0.9em;
margin-bottom: 0.5em;
}
#locations-panel-list .location-result .distance {
position: absolute;
top: 0.9em;
right: 0;
text-align: center;
font-size: 0.9em;
width: 5em;
}
#location-results-list {
list-style-type: none;
margin: 0;
padding: 0;
}
</style>
<script>
'use strict';
/**
* Defines an instance of the Locator+ solution, to be instantiated
* when the Maps library is loaded.
*/
function LocatorPlus(configuration) {
const locator = this;
locator.locations = configuration.locations || [];
locator.capabilities = configuration.capabilities || {};
const mapEl = document.getElementById('map');
const panelEl = document.getElementById('locations-panel');
locator.panelListEl = document.getElementById('locations-panel-list');
const sectionNameEl =
document.getElementById('location-results-section-name');
const resultsContainerEl = document.getElementById('location-results-list');
const itemsTemplate = Handlebars.compile(
document.getElementById('locator-result-items-tmpl').innerHTML);
locator.searchLocation = null;
locator.searchLocationMarker = null;
locator.selectedLocationIdx = null;
locator.userCountry = null;
// Initialize the map -------------------------------------------------------
locator.map = new google.maps.Map(mapEl, configuration.mapOptions);
// Store selection.
const selectResultItem = function(locationIdx, panToMarker, scrollToResult) {
locator.selectedLocationIdx = locationIdx;
for (let locationElem of resultsContainerEl.children) {
locationElem.classList.remove('selected');
if (getResultIndex(locationElem) === locator.selectedLocationIdx) {
locationElem.classList.add('selected');
if (scrollToResult) {
panelEl.scrollTop = locationElem.offsetTop;
}
}
}
if (panToMarker && (locationIdx != null)) {
locator.map.panTo(locator.locations[locationIdx].coords);
}
};
// Create a marker for each location.
const markers = locator.locations.map(function(location, index) {
const marker = new google.maps.Marker({
position: location.coords,
map: locator.map,
title: location.title,
});
marker.addListener('click', function() {
selectResultItem(index, false, true);
});
return marker;
});
// Fit map to marker bounds.
locator.updateBounds = function() {
const bounds = new google.maps.LatLngBounds();
if (locator.searchLocationMarker) {
bounds.extend(locator.searchLocationMarker.getPosition());
}
for (let i = 0; i < markers.length; i++) {
bounds.extend(markers[i].getPosition());
}
locator.map.fitBounds(bounds);
};
if (locator.locations.length) {
locator.updateBounds();
}
// Get the distance of a store location to the user's location,
// used in sorting the list.
const getLocationDistance = function(location) {
if (!locator.searchLocation) return null;
// Use travel distance if available (from Distance Matrix).
if (location.travelDistanceValue != null) {
return location.travelDistanceValue;
}
// Fall back to straight-line distance.
return google.maps.geometry.spherical.computeDistanceBetween(
new google.maps.LatLng(location.coords),
locator.searchLocation.location);
};
// Render the results list --------------------------------------------------
const getResultIndex = function(elem) {
return parseInt(elem.getAttribute('data-location-index'));
};
locator.renderResultsList = function() {
let locations = locator.locations.slice();
for (let i = 0; i < locations.length; i++) {
locations[i].index = i;
}
if (locator.searchLocation) {
sectionNameEl.textContent =
'Nearest locations (' + locations.length + ')';
locations.sort(function(a, b) {
return getLocationDistance(a) - getLocationDistance(b);
});
} else {
sectionNameEl.textContent = `All locations (${locations.length})`;
}
const resultItemContext = { locations: locations };
resultsContainerEl.innerHTML = itemsTemplate(resultItemContext);
for (let item of resultsContainerEl.children) {
const resultIndex = getResultIndex(item);
if (resultIndex === locator.selectedLocationIdx) {
item.classList.add('selected');
}
const resultSelectionHandler = function() {
selectResultItem(resultIndex, true, false);
};
// Clicking anywhere on the item selects this location.
// Additionally, create a button element to make this behavior
// accessible under tab navigation.
item.addEventListener('click', resultSelectionHandler);
item.querySelector('.select-location')
.addEventListener('click', function(e) {
resultSelectionHandler();
e.stopPropagation();
});
}
};
// Optional capability initialization --------------------------------------
initializeSearchInput(locator);
initializeDistanceMatrix(locator);
// Initial render of results -----------------------------------------------
locator.renderResultsList();
}
/** When the search input capability is enabled, initialize it. */
function initializeSearchInput(locator) {
const geocodeCache = new Map();
const geocoder = new google.maps.Geocoder();
const searchInputEl = document.getElementById('location-search-input');
const searchButtonEl = document.getElementById('location-search-button');
const updateSearchLocation = function(address, location) {
if (locator.searchLocationMarker) {
locator.searchLocationMarker.setMap(null);
}
if (!location) {
locator.searchLocation = null;
return;
}
locator.searchLocation = {'address': address, 'location': location};
locator.searchLocationMarker = new google.maps.Marker({
position: location,
map: locator.map,
title: 'My location',
icon: {
path: google.maps.SymbolPath.CIRCLE,
scale: 12,
fillColor: '#3367D6',
fillOpacity: 0.5,
strokeOpacity: 0,
}
});
// Update the locator's idea of the user's country, used for units. Use
// `formatted_address` instead of the more structured `address_components`
// to avoid an additional billed call.
const addressParts = address.split(' ');
locator.userCountry = addressParts[addressParts.length - 1];
// Update map bounds to include the new location marker.
locator.updateBounds();
// Update the result list so we can sort it by proximity.
locator.renderResultsList();
locator.updateTravelTimes();
};
const geocodeSearch = function(query) {
if (!query) {
return;
}
const handleResult = function(geocodeResult) {
searchInputEl.value = geocodeResult.formatted_address;
updateSearchLocation(
geocodeResult.formatted_address, geocodeResult.geometry.location);
};
if (geocodeCache.has(query)) {
handleResult(geocodeCache.get(query));
return;
}
const request = {address: query, bounds: locator.map.getBounds()};
geocoder.geocode(request, function(results, status) {
if (status === 'OK') {
if (results.length > 0) {
const result = results[0];
geocodeCache.set(query, result);
handleResult(result);
}
}
});
};
// Set up geocoding on the search input.
searchButtonEl.addEventListener('click', function() {
geocodeSearch(searchInputEl.value.trim());
});
// Initialize Autocomplete.
initializeSearchInputAutocomplete(
locator, searchInputEl, geocodeSearch, updateSearchLocation);
}
/** Add Autocomplete to the search input. */
function initializeSearchInputAutocomplete(
locator, searchInputEl, fallbackSearch, searchLocationUpdater) {
// Set up Autocomplete on the search input. Bias results to map viewport.
const autocomplete = new google.maps.places.Autocomplete(searchInputEl, {
types: ['geocode'],
fields: ['place_id', 'formatted_address', 'geometry.location']
});
autocomplete.bindTo('bounds', locator.map);
autocomplete.addListener('place_changed', function() {
const placeResult = autocomplete.getPlace();
if (!placeResult.geometry) {
// Hitting 'Enter' without selecting a suggestion will result in a
// placeResult with only the text input value as the 'name' field.
fallbackSearch(placeResult.name);
return;
}
searchLocationUpdater(
placeResult.formatted_address, placeResult.geometry.location);
});
}
/** Initialize Distance Matrix for the locator. */
function initializeDistanceMatrix(locator) {
const distanceMatrixService = new google.maps.DistanceMatrixService();
// Annotate travel times to the selected location using Distance Matrix.
locator.updateTravelTimes = function() {
if (!locator.searchLocation) return;
const units = (locator.userCountry === 'USA') ?
google.maps.UnitSystem.IMPERIAL :
google.maps.UnitSystem.METRIC;
const request = {
origins: [locator.searchLocation.location],
destinations: locator.locations.map(function(x) {
return x.coords;
}),
travelMode: google.maps.TravelMode.DRIVING,
unitSystem: units,
};
const callback = function(response, status) {
if (status === 'OK') {
const distances = response.rows[0].elements;
for (let i = 0; i < distances.length; i++) {
const distResult = distances[i];
let travelDistanceText, travelDistanceValue;
if (distResult.status === 'OK') {
travelDistanceText = distResult.distance.text;
travelDistanceValue = distResult.distance.value;
}
const location = locator.locations[i];
location.travelDistanceText = travelDistanceText;
location.travelDistanceValue = travelDistanceValue;
}
// Re-render the results list, in case the ordering has changed.
locator.renderResultsList();
}
};
distanceMatrixService.getDistanceMatrix(request, callback);
};
}
</script>
<script>
const CONFIGURATION = {
"locations": [
{"title":"Death Valley National Park","address1":"California","address2":"United States","coords":{"lat":36.4617,"lng":-116.8668},"placeId":"ChIJR4qudndLx4ARVLDye3zwycw"},
{"title":"Yosemite National Park","address1":"California","address2":"USA","coords":{"lat":37.7487,"lng":-119.5873},"placeId":"ChIJxeyK9Z3wloAR_gOA7SycJC0"},
{"title":"Joshua Tree National Park","address1":"California","address2":"USA","coords":{"lat":33.9157,"lng":-115.8807},"placeId":"ChIJe6hluYWP2oAR4p3rOqftdxk"},
{"title":"Lassen Volcanic National Park","address1":"California","address2":"USA","coords":{"lat":40.4377,"lng":-121.5338},"placeId":"ChIJvzhBwQdWnYARQmdmeqfYNI8"},
{"title":"Sequoia National Park","address1":"California","address2":"USA","coords":{"lat":36.491,"lng":-118.8253},"placeId":"ChIJeWUZLX37v4ARZPQen_nfCkQ"},
{"title":"Pinnacles National Park","address1":"California","address2":"USA","coords":{"lat":36.4938,"lng":-121.1465},"placeId":"ChIJn93OiYBDkoAR7kSomO77gps"},
{"title":"Redwood National and State Parks","address1":"California","address2":"USA","coords":{"lat":41.3025,"lng":-124.0471},"placeId":"ChIJpX9B9TZm0FQRjl87lWYyzzY"},
{"title":"Kings Canyon National Park","address1":"California","address2":"USA","coords":{"lat":36.74,"lng":-118.9633},"placeId":"ChIJe6fxX-7Vv4ARTA9DcLeDZII"}
],
"mapOptions": {"center":{"lat":38.0,"lng":-100.0},"fullscreenControl":true,"mapTypeControl":false,"streetViewControl":false,"zoom":4,"zoomControl":true,"maxZoom":17},
"mapsApiKey": "YOUR_API_KEY_HERE"
};
function initMap() {
new LocatorPlus(CONFIGURATION);
}
</script>
<script id="locator-result-items-tmpl" type="text/x-handlebars-template">
{{#each locations}}
<li class="location-result" data-location-index="{{index}}">
<button class="select-location">
<h2 class="name">{{title}}</h2>
</button>
<div class="address">{{address1}}<br>{{address2}}</div>
{{#if travelDistanceText}}
<div class="distance">{{travelDistanceText}}</div>
{{/if}}
</li>
{{/each}}
</script>
</head>
<body>
<div id="map-container">
<div id="locations-panel">
<div id="locations-panel-list">
<header>
<h1 class="search-title">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/place/v15/24px.svg"/>
Find a location near you
</h1>
<div class="search-input">
<input id="location-search-input" placeholder="Enter your address or zip code">
<div id="search-overlay-search" class="search-input-overlay search">
<button id="location-search-button">
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/search/v11/24px.svg" alt="Search"/>
</button>
</div>
</div>
</header>
<div class="section-name" id="location-results-section-name">
All locations
</div>
<div class="results">
<ul id="location-results-list"></ul>
</div>
</div>
</div>
<div id="map"></div>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&callback=initMap&libraries=places,geometry&solution_channel=GMP_QB_locatorplus_v4_cABD" async defer></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Neighborhood Discovery</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
<script src="https://ajax.googleapis.com/ajax/libs/handlebars/4.7.7/handlebars.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
}
.neighborhood-discovery {
box-sizing: border-box;
font-family: "Roboto", sans-serif;
height: 100%;
position: relative;
width: 100%;
}
.neighborhood-discovery a {
color: #4285f4;
text-decoration: none;
}
.neighborhood-discovery button {
background: none;
border: none;
color: inherit;
cursor: pointer;
font: inherit;
font-size: inherit;
padding: 0;
}
.neighborhood-discovery .info {
color: #555;
font-size: 0.9em;
margin-top: 0.3em;
}
.neighborhood-discovery .panel {
background: white;
bottom: 0;
box-sizing: border-box;
left: 0;
overflow-y: auto;
position: absolute;
top: 0;
width: 20em;
}
.neighborhood-discovery .panel.no-scroll {
overflow-y: hidden;
}
.neighborhood-discovery .photo {
background-color: #dadce0;
background-position: center;
background-size: cover;
border-radius: 0.3em;
cursor: pointer;
}
.neighborhood-discovery .navbar {
background: white;
position: sticky;
top: 0;
z-index: 2;
}
.neighborhood-discovery .right {
float: right;
}
.neighborhood-discovery .star-icon {
filter: invert(88%) sepia(60%) saturate(2073%) hue-rotate(318deg) brightness(93%) contrast(104%);
height: 1.2em;
margin-right: -0.3em;
margin-top: -0.08em;
vertical-align: top;
width: 1.2em;
}
.neighborhood-discovery .star-icon:last-child {
margin-right: 0.2em;
}
.neighborhood-discovery .map {
bottom: 0;
left: 20em;
position: absolute;
right: 0;
top: 0;
}
@media only screen and (max-width: 640px) {
.neighborhood-discovery .panel {
right: 0;
top: 50%;
width: unset;
}
.neighborhood-discovery .map {
bottom: 50%;
left: 0;
}
}
/* --------------------------- PLACES PANEL --------------------------- */
.neighborhood-discovery .places-panel {
box-shadow: 0 0 10px rgb(60 64 67 / 28%);
z-index: 1;
}
.neighborhood-discovery .places-panel header {
padding: 0.5em;
}
.neighborhood-discovery .search-input input {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0.3em;
box-sizing: border-box;
font-size: 1em;
height: 2.2em;
padding: 0 2.5em 0 1em;
width: 100%;
}
.neighborhood-discovery .search-input button {
position: absolute;
right: 0.8em;
top: 0.8em;
}
.neighborhood-discovery .show-more-button {
bottom: 0.5em;
display: none;
left: 28%;
line-height: 1.5em;
padding: 0.6em;
position: relative;
width: 44%;
}
.neighborhood-discovery .show-more-button.sticky {
background: white;
border-radius: 1.5em;
box-shadow: 0 4px 10px rgb(60 64 67 / 28%);
position: sticky;
z-index: 2;
}
.neighborhood-discovery .show-more-button:disabled {
opacity: 0.5;
}
.neighborhood-discovery .place-results-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.neighborhood-discovery .place-result {
border-top: 1px solid rgba(0, 0, 0, 0.12);
cursor: pointer;
display: flex;
padding: 0.8em;
}
.neighborhood-discovery .place-result .text {
flex-grow: 1;
}
.neighborhood-discovery .place-result .name {
font-size: 1em;
font-weight: 500;
text-align: left;
}
.neighborhood-discovery .place-result .photo {
flex: 0 0 4em;
height: 4em;
margin-left: 0.8em;
}
/* -------------------------- DETAILS PANEL --------------------------- */
.neighborhood-discovery .details-panel {
display: none;
z-index: 20;
}
.neighborhood-discovery .details-panel .back-button {
color: #4285f4;
padding: 0.9em;
}
.neighborhood-discovery .details-panel .back-button .icon {
/* Match link color #4285f4 */
filter: invert(47%) sepia(71%) saturate(2372%) hue-rotate(200deg) brightness(97%) contrast(98%);
height: 1.2em;
width: 1.2em;
vertical-align: bottom;
}
.neighborhood-discovery .details-panel header {
padding: 0.9em;
}
.neighborhood-discovery .details-panel h2 {
font-size: 1.4em;
font-weight: 400;
margin: 0;
}
.neighborhood-discovery .details-panel .section {
border-top: 1px solid rgba(0, 0, 0, 0.12);
padding: 0.9em;
}
.neighborhood-discovery .details-panel .contact {
align-items: center;
display: flex;
font-size: 0.9em;
margin: 0.8em 0;
}
.neighborhood-discovery .details-panel .contact .icon {
width: 1.5em;
height: 1.5em;
}
.neighborhood-discovery .details-panel .contact .text {
margin-left: 1em;
}
.neighborhood-discovery .details-panel .contact .weekday {
display: inline-block;
width: 5em;
}
.neighborhood-discovery .details-panel .photos {
text-align: center;
}
.neighborhood-discovery .details-panel .photo {
display: inline-block;
height: 5.5em;
width: 5.5em;
}
.neighborhood-discovery .details-panel .review {
margin-top: 1.2em;
}
.neighborhood-discovery .details-panel .review .reviewer-avatar {
background-repeat: no-repeat;
background-size: cover;
float: left;
height: 1.8em;
margin-right: 0.8em;
width: 1.8em;
}
.neighborhood-discovery .details-panel .review .reviewer-name {
color: #202124;
font-weight: 500;
line-height: 1.8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.neighborhood-discovery .details-panel .review .rating {
margin: 0.5em 0;
}
.neighborhood-discovery .details-panel .attribution {
color: #777;
margin: 0;
font-size: 0.8em;
font-style: italic;
}
/* --------------------------- PHOTO MODAL ---------------------------- */
.neighborhood-discovery .photo-modal {
background: rgba(0, 0, 0, 0.8);
display: none;
height: 100%;
position: fixed;
width: 100%;
z-index: 100;
}
.neighborhood-discovery .photo-modal > img {
bottom: 0;
left: 0;
margin: auto;
max-height: 100%;
max-width: 100%;
position: absolute;
right: 0;
top: 0;
}
.neighborhood-discovery .photo-modal > div {
border-radius: 0.4em;
color: white;
background: rgba(0, 0, 0, 0.6);
margin: 1em;
padding: 0.9em;
position: absolute;
}
.neighborhood-discovery .photo-modal .back-button .icon {
filter: brightness(0) invert(1);
margin: 0.4em 0.6em 0 0;
}
.neighborhood-discovery .photo-modal .photo-text {
float: right;
}
.neighborhood-discovery .photo-modal .photo-attrs {
font-size: 0.8em;
margin-top: 0.3em;
}
</style>
<script>
'use strict';
/** Hides a DOM element and optionally focuses on focusEl. */
function hideElement(el, focusEl) {
el.style.display = 'none';
if (focusEl) focusEl.focus();
}
/** Shows a DOM element that has been hidden and optionally focuses on focusEl. */
function showElement(el, focusEl) {
el.style.display = 'block';
if (focusEl) focusEl.focus();
}
/** Determines if a DOM element contains content that cannot be scrolled into view. */
function hasHiddenContent(el) {
const noscroll = window.getComputedStyle(el).overflowY.includes('hidden');
return noscroll && el.scrollHeight > el.clientHeight;
}
/** Format a Place Type string by capitalizing and replacing underscores with spaces. */
function formatPlaceType(str) {
const capitalized = str.charAt(0).toUpperCase() + str.slice(1);
return capitalized.replace(/_/g, ' ');
}
/** Initializes an array of zeros with the given size. */
function initArray(arraySize) {
const array = [];
while (array.length < arraySize) {
array.push(0);
}
return array;
}
/** Assigns star icons to an object given its rating (out of 5). */
function addStarIcons(obj) {
if (!obj.rating) return;
const starsOutOfTen = Math.round(2 * obj.rating);
const fullStars = Math.floor(starsOutOfTen / 2);
const halfStars = fullStars !== starsOutOfTen / 2 ? 1 : 0;
const emptyStars = 5 - fullStars - halfStars;
// Express stars as arrays to make iterating in Handlebars easy.
obj.fullStarIcons = initArray(fullStars);
obj.halfStarIcons = initArray(halfStars);
obj.emptyStarIcons = initArray(emptyStars);
}
/**
* Constructs an array of opening hours by day from a PlaceOpeningHours object,
* where adjacent days of week with the same hours are collapsed into one element.
*/
function parseDaysHours(openingHours) {
const daysHours = openingHours.weekday_text.map((e) => e.split(/\:\s+/))
.map((e) => ({'days': e[0].substr(0, 3), 'hours': e[1]}));
for (let i = 1; i < daysHours.length; i++) {
if (daysHours[i - 1].hours === daysHours[i].hours) {
if (daysHours[i - 1].days.indexOf('-') !== -1) {
daysHours[i - 1].days =
daysHours[i - 1].days.replace(/\w+$/, daysHours[i].days);
} else {
daysHours[i - 1].days += ' - ' + daysHours[i].days;
}
daysHours.splice(i--, 1);
}
}
return daysHours;
}
/** Number of POIs to show on widget load. */
const ND_NUM_PLACES_INITIAL = 5;
/** Number of additional POIs to show when 'Show More' button is clicked. */
const ND_NUM_PLACES_SHOW_MORE = 5;
/** Maximum number of place photos to show on the details panel. */
const ND_NUM_PLACE_PHOTOS_MAX = 6;
/** Minimum zoom level at which the default map POI pins will be shown. */
const ND_DEFAULT_POI_MIN_ZOOM = 18;
/** Mapping of Place Types to Material Icons used to render custom map markers. */
const ND_MARKER_ICONS_BY_TYPE = {
// Full list of icons can be found at https://fonts.google.com/icons
'_default': 'circle',
'restaurant': 'restaurant',
'cafe': 'local_cafe',
'bar': 'local_bar',
'park': 'park',
'museum': 'museum',
'supermarket': 'local_grocery_store',
'clothing_store': 'local_mall',
'department_store': 'local_mall',
'shopping_mall': 'local_mall',
'primary_school': 'school',
'secondary_school': 'school',
};
/**
* Defines an instance of the Neighborhood Discovery widget, to be
* instantiated when the Maps library is loaded.
*/
function NeighborhoodDiscovery(configuration) {
const widget = this;
const widgetEl = document.querySelector('.neighborhood-discovery');
widget.center = configuration.mapOptions.center;
widget.places = configuration.pois || [];
// Initialize core functionalities -------------------------------------
initializeMap();
initializePlaceDetails();
initializeSidePanel();
// Initialize additional capabilities ----------------------------------
initializeSearchInput();
// Initializer function definitions ------------------------------------
/** Initializes the interactive map and adds place markers. */
function initializeMap() {
const mapOptions = configuration.mapOptions;
widget.mapBounds = new google.maps.Circle(
{center: widget.center, radius: configuration.mapRadius}).getBounds();
mapOptions.restriction = {latLngBounds: widget.mapBounds};
mapOptions.mapTypeControlOptions = {position: google.maps.ControlPosition.TOP_RIGHT};
widget.map = new google.maps.Map(widgetEl.querySelector('.map'), mapOptions);
widget.map.fitBounds(widget.mapBounds, /* padding= */ 0);
widget.map.addListener('click', (e) => {
// Check if user clicks on a POI pin from the base map.
if (e.placeId) {
e.stop();
widget.selectPlaceById(e.placeId);
}
});
widget.map.addListener('zoom_changed', () => {
// Customize map styling to show/hide default POI pins or text based on zoom level.
const hideDefaultPoiPins = widget.map.getZoom() < ND_DEFAULT_POI_MIN_ZOOM;
widget.map.setOptions({
styles: [{
featureType: 'poi',
elementType: hideDefaultPoiPins ? 'labels' : 'labels.text',
stylers: [{visibility: 'off'}],
}],
});
});
const markerPath = widgetEl.querySelector('.marker-pin path').getAttribute('d');
const drawMarker = function(title, position, fillColor, strokeColor, labelText) {
return new google.maps.Marker({
title: title,
position: position,
map: widget.map,
icon: {
path: markerPath,
fillColor: fillColor,
fillOpacity: 1,
strokeColor: strokeColor,
anchor: new google.maps.Point(13, 35),
labelOrigin: new google.maps.Point(13, 13),
},
label: {
text: labelText,
color: 'white',
fontSize: '16px',
fontFamily: 'Material Icons',
},
});
};
// Add marker for the specified Place object.
widget.addPlaceMarker = function(place) {
place.marker = drawMarker(place.name, place.coords, '#EA4335', '#C5221F', place.icon);
place.marker.addListener('click', () => void widget.selectPlaceById(place.placeId));
};
// Fit map to bounds that contain all markers of the specified Place objects.
widget.updateBounds = function(places) {
const bounds = new google.maps.LatLngBounds();
bounds.extend(widget.center);
for (let place of places) {
bounds.extend(place.marker.getPosition());
}
widget.map.fitBounds(bounds, /* padding= */ 100);
};
// Marker used to highlight a place from Autocomplete search.
widget.selectedPlaceMarker = new google.maps.Marker({title: 'Point of Interest'});
}
/** Initializes Place Details service for the widget. */
function initializePlaceDetails() {
const detailsService = new google.maps.places.PlacesService(widget.map);
const placeIdsToDetails = new Map(); // Create object to hold Place results.
for (let place of widget.places) {
placeIdsToDetails.set(place.placeId, place);
place.fetchedFields = new Set(['place_id']);
}
widget.fetchPlaceDetails = function(placeId, fields, callback) {
if (!placeId || !fields) return;
// Check for field existence in Place object.
let place = placeIdsToDetails.get(placeId);
if (!place) {
place = {placeId: placeId, fetchedFields: new Set(['place_id'])};
placeIdsToDetails.set(placeId, place);
}
const missingFields = fields.filter((field) => !place.fetchedFields.has(field));
if (missingFields.length === 0) {
callback(place);
return;
}
const request = {placeId: placeId, fields: missingFields};
let retryCount = 0;
const processResult = function(result, status) {
if (status !== google.maps.places.PlacesServiceStatus.OK) {
// If query limit has been reached, wait before making another call;
// Increase wait time of each successive retry with exponential backoff
// and terminate after five failed attempts.
if (status === google.maps.places.PlacesServiceStatus.OVER_QUERY_LIMIT &&
retryCount < 5) {
const delay = (Math.pow(2, retryCount) + Math.random()) * 500;
setTimeout(() => void detailsService.getDetails(request, processResult), delay);
retryCount++;
}
return;
}
// Basic details.
if (result.name) place.name = result.name;
if (result.geometry) place.coords = result.geometry.location;
if (result.formatted_address) place.address = result.formatted_address;
if (result.photos) {
place.photos = result.photos.map((photo) => ({
urlSmall: photo.getUrl({maxWidth: 200, maxHeight: 200}),
urlLarge: photo.getUrl({maxWidth: 1200, maxHeight: 1200}),
attrs: photo.html_attributions,
})).slice(0, ND_NUM_PLACE_PHOTOS_MAX);
}
if (result.types) {
place.type = formatPlaceType(result.types[0]);
place.icon = ND_MARKER_ICONS_BY_TYPE['_default'];
for (let type of result.types) {
if (type in ND_MARKER_ICONS_BY_TYPE) {
place.type = formatPlaceType(type);
place.icon = ND_MARKER_ICONS_BY_TYPE[type];
break;
}
}
}
if (result.url) place.url = result.url;
// Contact details.
if (result.website) {
place.website = result.website;
const url = new URL(place.website);
place.websiteDomain = url.hostname;
}
if (result.formatted_phone_number) place.phoneNumber = result.formatted_phone_number;
if (result.opening_hours) place.openingHours = parseDaysHours(result.opening_hours);
// Review details.
if (result.rating) {
place.rating = result.rating;
addStarIcons(place);
}
if (result.user_ratings_total) place.numReviews = result.user_ratings_total;
if (result.price_level) {
place.priceLevel = result.price_level;
place.dollarSigns = initArray(result.price_level);
}
if (result.reviews) {
place.reviews = result.reviews;
for (let review of place.reviews) {
addStarIcons(review);
}
}
for (let field of missingFields) {
place.fetchedFields.add(field);
}
callback(place);
};
// Use result from Place Autocomplete if available.
if (widget.placeIdsToAutocompleteResults) {
const autoCompleteResult = widget.placeIdsToAutocompleteResults.get(placeId);
if (autoCompleteResult) {
processResult(autoCompleteResult, google.maps.places.PlacesServiceStatus.OK);
return;
}
}
detailsService.getDetails(request, processResult);
};
}
/** Initializes the side panel that holds curated POI results. */
function initializeSidePanel() {
const placesPanelEl = widgetEl.querySelector('.places-panel');
const detailsPanelEl = widgetEl.querySelector('.details-panel');
const placeResultsEl = widgetEl.querySelector('.place-results-list');
const showMoreButtonEl = widgetEl.querySelector('.show-more-button');
const photoModalEl = widgetEl.querySelector('.photo-modal');
const detailsTemplate = Handlebars.compile(
document.getElementById('nd-place-details-tmpl').innerHTML);
const resultsTemplate = Handlebars.compile(
document.getElementById('nd-place-results-tmpl').innerHTML);
// Show specified POI photo in a modal.
const showPhotoModal = function(photo, placeName) {
const prevFocusEl = document.activeElement;
const imgEl = photoModalEl.querySelector('img');
imgEl.src = photo.urlLarge;
const backButtonEl = photoModalEl.querySelector('.back-button');
backButtonEl.addEventListener('click', () => {
hideElement(photoModalEl, prevFocusEl);
imgEl.src = '';
});
photoModalEl.querySelector('.photo-place').innerHTML = placeName;
photoModalEl.querySelector('.photo-attrs span').innerHTML = photo.attrs;
const attributionEl = photoModalEl.querySelector('.photo-attrs a');
if (attributionEl) attributionEl.setAttribute('target', '_blank');
photoModalEl.addEventListener('click', (e) => {
if (e.target === photoModalEl) {
hideElement(photoModalEl, prevFocusEl);
imgEl.src = '';
}
});
showElement(photoModalEl, backButtonEl);
};
// Select a place by id and show details.
let selectedPlaceId;
widget.selectPlaceById = function(placeId, panToMarker) {
if (selectedPlaceId === placeId) return;
selectedPlaceId = placeId;
const prevFocusEl = document.activeElement;
const showDetailsPanel = function(place) {
detailsPanelEl.innerHTML = detailsTemplate(place);
const backButtonEl = detailsPanelEl.querySelector('.back-button');
backButtonEl.addEventListener('click', () => {
hideElement(detailsPanelEl, prevFocusEl);
selectedPlaceId = undefined;
widget.selectedPlaceMarker.setMap(null);
});
detailsPanelEl.querySelectorAll('.photo').forEach((photoEl, i) => {
photoEl.addEventListener('click', () => {
showPhotoModal(place.photos[i], place.name);
});
});
showElement(detailsPanelEl, backButtonEl);
detailsPanelEl.scrollTop = 0;
};
const processResult = function(place) {
if (place.marker) {
widget.selectedPlaceMarker.setMap(null);
} else {
widget.selectedPlaceMarker.setPosition(place.coords);
widget.selectedPlaceMarker.setMap(widget.map);
}
if (panToMarker) {
widget.map.panTo(place.coords);
}
showDetailsPanel(place);
};
widget.fetchPlaceDetails(placeId, [
'name', 'types', 'geometry.location', 'formatted_address', 'photo', 'url',
'website', 'formatted_phone_number', 'opening_hours',
'rating', 'user_ratings_total', 'price_level', 'review',
], processResult);
};
// Render the specified place objects and append them to the POI list.
const renderPlaceResults = function(places, startIndex) {
placeResultsEl.insertAdjacentHTML('beforeend', resultsTemplate({places: places}));
placeResultsEl.querySelectorAll('.place-result').forEach((resultEl, i) => {
const place = places[i - startIndex];
if (!place) return;
// Clicking anywhere on the item selects the place.
// Additionally, create a button element to make this behavior
// accessible under tab navigation.
resultEl.addEventListener('click', () => {
widget.selectPlaceById(place.placeId, /* panToMarker= */ true);
});
resultEl.querySelector('.name').addEventListener('click', (e) => {
widget.selectPlaceById(place.placeId, /* panToMarker= */ true);
e.stopPropagation();
});
resultEl.querySelector('.photo').addEventListener('click', (e) => {
showPhotoModal(place.photos[0], place.name);
e.stopPropagation();
});
widget.addPlaceMarker(place);
});
};
// Index of next Place object to show in the POI list.
let nextPlaceIndex = 0;
// Fetch and show basic info for the next N places.
const showNextPlaces = function(n) {
const nextPlaces = widget.places.slice(nextPlaceIndex, nextPlaceIndex + n);
if (nextPlaces.length < 1) {
hideElement(showMoreButtonEl);
return;
}
showMoreButtonEl.disabled = true;
// Keep track of the number of Places calls that have not finished.
let count = nextPlaces.length;
for (let place of nextPlaces) {
const processResult = function(place) {
count--;
if (count > 0) return;
renderPlaceResults(nextPlaces, nextPlaceIndex);
nextPlaceIndex += n;
widget.updateBounds(widget.places.slice(0, nextPlaceIndex));
const hasMorePlacesToShow = nextPlaceIndex < widget.places.length;
if (hasMorePlacesToShow || hasHiddenContent(placesPanelEl)) {
showElement(showMoreButtonEl);
showMoreButtonEl.disabled = false;
} else {
hideElement(showMoreButtonEl);
}
};
widget.fetchPlaceDetails(place.placeId, [
'name', 'types', 'geometry.location',
'photo',
'rating', 'user_ratings_total', 'price_level',
], processResult);
}
};
showNextPlaces(ND_NUM_PLACES_INITIAL);
showMoreButtonEl.addEventListener('click', () => {
placesPanelEl.classList.remove('no-scroll');
showMoreButtonEl.classList.remove('sticky');
showNextPlaces(ND_NUM_PLACES_SHOW_MORE);
});
}
/** Initializes Search Input for the widget. */
function initializeSearchInput() {
const searchInputEl = widgetEl.querySelector('.place-search-input');
widget.placeIdsToAutocompleteResults = new Map();
// Set up Autocomplete on the search input.
const autocomplete = new google.maps.places.Autocomplete(searchInputEl, {
types: ['establishment'],
fields: [
'place_id', 'name', 'types', 'geometry.location', 'formatted_address', 'photo', 'url',
'website', 'formatted_phone_number', 'opening_hours',
'rating', 'user_ratings_total', 'price_level', 'review',
],
bounds: widget.mapBounds,
strictBounds: true,
});
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
widget.placeIdsToAutocompleteResults.set(place.place_id, place);
widget.selectPlaceById(place.place_id, /* panToMarker= */ true);
searchInputEl.value = '';
});
}
}
</script>
<script>
const CONFIGURATION = {
"pois": [
{"placeId": "ChIJO7l_l7f8cQ0Rf6IhEu_RjYA"},
{"placeId": "ChIJtRFSAVrjcQ0Rjs7Ibtt2vtY"},
{"placeId": "ChIJ45mdhb78cQ0R3qfxRyTkzgI"},
{"placeId": "ChIJ0cmHHpH8cQ0RbpTJjlVri2k"},
{"placeId": "ChIJ-TyL8778cQ0RGwyNKqccDOA"},
{"placeId": "ChIJrYQTtPL8cQ0RU_vmc_NqB8A"},
{"placeId": "ChIJLTqlF7_8cQ0RHosavBRI48Q"},
{"placeId": "ChIJj6h_ML78cQ0RIFvCcFCLHRU"},
{"placeId": "ChIJeWZxaLv8cQ0RlNvp2-m1x28"},
{"placeId": "ChIJn2VauvL8cQ0R6Y5EcRRXztM"},
{"placeId": "ChIJoZxr8r78cQ0RQuqduxRbyRM"},
{"placeId": "ChIJ79TXfZX8cQ0RoeuqRI2yyno"},
{"placeId": "ChIJjQaYi5b8cQ0RPOQ7xwds524"},
{"placeId": "ChIJD0NYA-v8cQ0RWIwK74oOAOQ"},
{"placeId": "ChIJY1N6W578cQ0RlUdw_5yVKic"},
{"placeId": "ChIJjw4K1Jn8cQ0RET2g24bTW5c"},
{"placeId": "ChIJ6V0eyOn8cQ0R2mbceLx1lUU"},
{"placeId": "ChIJhTsy4738cQ0Rnw06gpIVjOU"},
{"placeId": "ChIJ3dNkAb78cQ0Rrx1OLzKumxY"},
{"placeId": "ChIJr3rLtsn8cQ0RUAoENt6qyqA"},
{"placeId": "ChIJMaFlqK_8cQ0RrIHsW7KXvT8"}
],
"mapRadius": 3000,
"mapOptions": {"center":{"lat":37.1773363,"lng":-3.5985571},"fullscreenControl":true,"mapTypeControl":true,"streetViewControl":false,"zoom":16,"zoomControl":true,"maxZoom":20},
"mapsApiKey": "YOUR_API_KEY_HERE"
};
function initMap() {
new NeighborhoodDiscovery(CONFIGURATION);
}
</script>
<script id="nd-place-results-tmpl" type="text/x-handlebars-template">
{{#each places}}
<li class="place-result">
<div class="text">
<button class="name">{{name}}</button>
<div class="info">
{{#if rating}}
<span>{{rating}}</span>
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="rating" class="star-icon"/>
{{/if}}
{{#if numReviews}}
<span>&nbsp;({{numReviews}})</span>
{{/if}}
{{#if priceLevel}}
&#183;&nbsp;<span>{{#each dollarSigns}}${{/each}}&nbsp;</span>
{{/if}}
</div>
<div class="info">{{type}}</div>
</div>
<button class="photo" style="background-image:url({{photos.0.urlSmall}})" aria-label="show photo in viewer"></button>
</li>
{{/each}}
</script>
<script id="nd-place-details-tmpl" type="text/x-handlebars-template">
<div class="navbar">
<button class="back-button">
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/arrow_back/v11/24px.svg" alt="back"/>
Back
</button>
</div>
<header>
<h2>{{name}}</h2>
<div class="info">
{{#if rating}}
<span class="star-rating-numeric">{{rating}}</span>
<span>
{{#each fullStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="full star" class="star-icon"/>
{{/each}}
{{#each halfStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_half/v17/24px.svg" alt="half star" class="star-icon"/>
{{/each}}
{{#each emptyStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_outline/v9/24px.svg" alt="empty star" class="star-icon"/>
{{/each}}
</span>
{{/if}}
{{#if numReviews}}
<a href="{{url}}" target="_blank">{{numReviews}} reviews</a>
{{else}}
<a href="{{url}}" target="_blank">See on Google Maps</a>
{{/if}}
{{#if priceLevel}}
&#183;
<span class="price-dollars">
{{#each dollarSigns}}${{/each}}
</span>
{{/if}}
</div>
{{#if type}}
<div class="info">{{type}}</div>
{{/if}}
</header>
<div class="section">
{{#if address}}
<div class="contact">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/place/v10/24px.svg" alt="Address" class="icon"/>
<div class="text">
{{address}}
</div>
</div>
{{/if}}
{{#if website}}
<div class="contact">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/public/v10/24px.svg" alt="Website" class="icon"/>
<div class="text">
<a href="{{website}}" target="_blank">{{websiteDomain}}</a>
</div>
</div>
{{/if}}
{{#if phoneNumber}}
<div class="contact">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/phone/v10/24px.svg" alt="Phone number" class="icon"/>
<div class="text">
{{phoneNumber}}
</div>
</div>
{{/if}}
{{#if openingHours}}
<div class="contact">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/schedule/v12/24px.svg" alt="Opening hours" class="icon"/>
<div class="text">
{{#each openingHours}}
<div>
<span class="weekday">{{days}}</span>
<span class="hours">{{hours}}</span>
</div>
{{/each}}
</div>
</div>
{{/if}}
</div>
{{#if photos}}
<div class="photos section">
{{#each photos}}
<button class="photo" style="background-image:url({{urlSmall}})" aria-label="show photo in viewer"></button>
{{/each}}
</div>
{{/if}}
{{#if reviews}}
<div class="reviews section">
<p class="attribution">Reviews by Google users</p>
{{#each reviews}}
<div class="review">
<a class="reviewer-identity" href="{{author_url}}" target="_blank">
<div class="reviewer-avatar" style="background-image:url({{profile_photo_url}})"></div>
<div class="reviewer-name">{{author_name}}</div>
</a>
<div class="rating info">
<span>
{{#each fullStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="full star" class="star-icon"/>
{{/each}}
{{#each halfStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_half/v17/24px.svg" alt="half star" class="star-icon"/>
{{/each}}
{{#each emptyStarIcons}}
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_outline/v9/24px.svg" alt="empty star" class="star-icon"/>
{{/each}}
</span>
<span class="review-time">{{relative_time_description}}</span>
</div>
<div class="info">{{text}}</div>
</div>
{{/each}}
</div>
{{/if}}
{{#if html_attributions}}
<div class="section">
{{#each html_attributions}}
<p class="attribution">{{{this}}}</p>
{{/each}}
</div>
{{/if}}
</script>
</head>
<body>
<div class="neighborhood-discovery">
<div class="places-panel panel no-scroll">
<header class="navbar">
<div class="search-input">
<input class="place-search-input" placeholder="Search nearby places">
<button class="place-search-button">
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/search/v11/24px.svg" alt="search"/>
</button>
</div>
</header>
<div class="results">
<ul class="place-results-list"></ul>
</div>
<button class="show-more-button sticky">
<span>Show More</span>
<img class="right" src="https://fonts.gstatic.com/s/i/googlematerialicons/expand_more/v11/24px.svg" alt="expand"/>
</button>
</div>
<div class="details-panel panel"></div>
<div class="map"></div>
<div class="photo-modal">
<img alt="place photo"/>
<div>
<button class="back-button">
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/arrow_back/v11/24px.svg" alt="back"/>
</button>
<div class="photo-text">
<div class="photo-place"></div>
<div class="photo-attrs">Photo by <span></span></div>
</div>
</div>
</div>
<svg class="marker-pin" xmlns="http://www.w3.org/2000/svg" width="26" height="38" fill="none">
<path d="M13 0C5.817 0 0 5.93 0 13.267c0 7.862 5.59 10.81 9.555 17.624C12.09 35.248 11.342 38 13 38c1.723 0 .975-2.817 3.445-7.043C20.085 24.503 26 21.162 26 13.267 26 5.93 20.183 0 13 0Z"/>
</svg>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&callback=initMap&libraries=places,geometry&solution_channel=GMP_QB_neighborhooddiscovery_v1_cADEF" async defer></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment