Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

flotiq profile imagelikayeltsova
likayeltsova forflotiq

Posted on • Originally published atflotiq.com

     

Build Store locator using Flotiq Headless CMS

Flotiq plays really well with location data, especially once you get to use our search capabilities. In this quick tutorial we'll build a very simple web page that displays locations on a map and allows to easily search your location database.

What we'll need:

  • Flotiq account
  • AStore Content Type Definition
  • SomeStore objects
  • A scoped API key
  • Some HTML, CSS and plain Javascript (to keep things simple).

And here's a sneak peak at what we'll build.

store-locator-flotiq

Setting up Flotiq

Once youregistered a Flotiq account and logged in, you'll have to create a new Content Type Definition. This is how you tell Flotiq what kind of data you will be dealing with. Mine looks like this:

flotiq store locator content type

I recommend that you use the same labels - it will be easier to follow the rest of the tutorial.

Next - create several Content Objects under theStore type. I added 3 shops in central London:

stores locations in grid

Finally - setup a scoped API key - you'll need it in a moment.

Building the page

We will now build a very simple page, which will display the store's locations on a map. Let's start with the basics.

Scaffolding

Ourindex.html:

<html><head>    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <link rel="stylesheet" href="index.css"></head><body>    <script src="index.js"></script></body></html>
Enter fullscreen modeExit fullscreen mode

We will be using Google Maps and Leaflet to display the locations on a map, so let's pull in the necessary scripts.

For Google Maps:

<body><!-- ... -->    <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_MAPS_KEY" async defer></script></body>
Enter fullscreen modeExit fullscreen mode

And Leaflet:

<head><!-- Add this into <head> : --> <link href="https://fonts.googleapis.com/icon?family=Material+Icons"        rel="stylesheet">    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"        integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="        crossorigin="" />    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css">    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css">    <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"         integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z"         crossorigin="anonymous"></head><body><!-- Add this before </body> --> <script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"        integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="        crossorigin=""></script>   <script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>    <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>    <script src='https://unpkg.com/leaflet.gridlayer.googlemutant@latest/Leaflet.GoogleMutant.js'></script></body>
Enter fullscreen modeExit fullscreen mode

Now, add a map container to your<body>:

<body>    <div data-map></div></body>
Enter fullscreen modeExit fullscreen mode

And you can now initialize the map, by adding this to yourindex.js:

document.querySelectorAll('[data-map]').forEach(function (mapContainer) {    /**     * Create:     * - Map and its view layer     * - geocoder control (to search cities)     * - create variable globalGroup for cluster group which will be created on first load (see onMarkersLoaded)     */    let map = L.map(mapContainer).setView([51.1079, 17.0385], 17);    let globalGroup;    let googleMaps = L.gridLayer.googleMutant({      type: 'roadmap'   // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'    }).addTo(map);    let osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {       attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'    });    L.control.layers({"Google Maps": googleMaps, "Open Street Map": osm}, null, {position: "bottomleft"}).addTo(map);  });
Enter fullscreen modeExit fullscreen mode

Note
Thanks to Leaflet - it's super simple to use both Google Maps and Open Street Map as our map providers. You can leave both, or choose one and remove the other. If you stick with Google, though, please remember to provide your Google Maps API key.

Finally - to make your map render in the browser, set the proper size in theindex.css file:

body, html {    padding: 0;    margin: 0;}.map-container {    width: 100%;    height: 700px;}
Enter fullscreen modeExit fullscreen mode

Once this is done - you should see a map rendered in your browser:First map rendered

Note
I intentionally skipped some minor details from the above implementation. Check the full source code in 01-scaffolding folder in this repo.

Pulling data

We will now pull store data from Flotiq and display it on the map. First - put these helper constants and variables at the top of theindex.js file:

const TOKEN = 'YOUR_TOKEN';const API_URL = 'https://api.flotiq.com';const CONTENT_TYPE = 'Store';const LOCATION_FIELD = 'Location';const LIMIT = 1000;const CLEAN_MAP_AT_ZOOM = 9;let localMarkers = {};let lastLoad = 0;let currentPoint = {  lat: 51.1079,  lng: 17.0385};let placeCollection = {};let doNotUpdate = false;
Enter fullscreen modeExit fullscreen mode

You will need to populate theTOKEN constant with your API key, copied from Flotiq.

Warning
We strongly recommend to create a dedicated key for every application you build and explicitly define the access scope to cover only the least amount of privileges required for a given app. Read more about Flotiq's scoped API keys in our docs.

Now, define theloadMarkers() function, like this:

/** * Load store markers to the visible map area * @param {L.Map} map  * @param {function([Array])} onLoad  */function loadMarkers(map, onLoad) {  const center = map.getCenter();  const radius = map.getCenter().distanceTo(map.getBounds().getNorthEast());  const url = API_URL    + '/api/v1/search?'    + [      'q=*',      'geo_filters[' + LOCATION_FIELD + ']=geo_distance,'         + radius + 'm,'         + center.lat + ','         + center.lng,      'content_type[]=' + CONTENT_TYPE,      'limit=' + LIMIT,      'auth_token=' + TOKEN    ].join('&');  fetch(url)    .then(function (res) { return res.json() })    .then(onLoad)}
Enter fullscreen modeExit fullscreen mode

The most important part of this function is how you should setup the search query, particularly thegeo_filters parameter. With the parameters defined above - we will be asking Flotiq's API to provide all locations within a given radius from the center of the map. You can read more about the/search API endpoint in theFlotiq search API docs.

TheloadMarkers() function will be used to pass the results pulled from Flotiq's API to theonMarkersLoaded() function, which will then render the location markers. The resulting object will look similar to:

{    "total_count": 2,    "count": 2,    "total_pages": 1,    "current_page": 1,    "summary": {        "aggregations": []    },    "data": [        {            "item": {                "id": "Store-516751",                "City": "London",                "Street": "50 Chandos Place",                "Country": "United Kindgom",                "Location": {                    "lat": 51.509768,                    "lon": -0.51105                },                "internal": {                    "createdAt": "2020-09-27T11:43:11+00:00",                    "deletedAt": "",                    "updatedAt": "2020-09-27T11:44:14+00:00",                    "contentType": "Store",                    "workflow_state": "saved"                },                "StoreName": "M & S Newsagents",                "created_at": "2020-09-27T11:43:11+00:00",                "updated_at": "2020-09-27T11:44:14+00:00",                "deleted_at": null,                "@timestamp": "2020-09-27T11:44:14+00:00"            },            "score": 4.1335316        }    ]}
Enter fullscreen modeExit fullscreen mode

If you defined the Content Type exactly as I did, you'll have the latitude and longitude stored in theitem.Location property of each of the entries of the data array.

Now, let's work out how the data should be displayed on the map, once it's pulled.

TheonMarkersLoaded() function will be used to put markers on the map.

  /**   * Register mmarkers for display within the map using clustering group   * @param {Array} collection    */  function onMarkersLoaded(collection) {    placeCollection = collection;    for (let i = 0; i < collection.data.length; i++) {      let store = collection.data[i].item;      if (!globalGroup) {        /**         * if the global clustering group for markers doesn't exist, then create it         */        globalGroup = L.markerClusterGroup();        map.addLayer(globalGroup)      }      if (localMarkers[store.id])        continue;      localMarkers[store.id] = L.marker([store.Location.lat, store.Location.lon]);      globalGroup.addLayer(localMarkers[store.id]);    }  }
Enter fullscreen modeExit fullscreen mode

Next, load the markers! Add this, below theonMarkersLoaded definition.

  loadMarkers(map, onMarkersLoaded);
Enter fullscreen modeExit fullscreen mode

When you reload the page - you should now see the markers appear on your map.

First markers appear on map

Note
Again - you can verify your progress in 02-pull-data folder in this repo.

Handling events

We will now add several event handlers that will refresh the list of locations when the map is moved or zoomed and we will enable navigating to the current location, based on Leaflet'smap.locate() method.

Add the event handlers:

/**   * Load data and Handle markers:   * - after initial loading   * - after panning   * - after zoom changed   */  map.on('move', function (e) {    loadMarkers(map, onMarkersLoaded);    doNotUpdate = true;  });  map.on('moveend', function (e) {    doNotUpdate = false;    //renderList(placeCollection);  })  map.on('zoomend', function () {    if (!globalGroup){      return;    }    if (map.getZoom() <= CLEAN_MAP_AT_ZOOM && map.hasLayer(globalGroup)) {      map.removeLayer(globalGroup);    }    if (map.getZoom() > CLEAN_MAP_AT_ZOOM && map.hasLayer(globalGroup) == false) {      map.addLayer(globalGroup);      loadMarkers(map, onMarkersLoaded);    }  });
Enter fullscreen modeExit fullscreen mode

You can now verify if indeed moving around the map will load new markers. Let's now enable positioning to our current location - once we receive the current point coordinates from the browser - we will update the map's center.

  function getCurrentLocation() {    // When the browser provides our location     // move the map to the point provided.    map.locate({      setView: true,      enableHighAccuracy: true,      maxZoom: 14    }).on('locationfound', function(data) {      // Set map center      currentPoint = {        lat: data.latlng.lat,        lng: data.latlng.lng      }    }).on('locationerror', function() {      console.log("Could not find location");    })  }  document.getElementById('currentLocation').addEventListener('click', getCurrentLocation);
Enter fullscreen modeExit fullscreen mode

We will now need to add a navigation box on top of the map. Add this under<body> in your index.html.

 <div>        <div>            <div>                <div>        <div><a title="Close"><span></span></a></div>                    <h1>FIND STORES NEAR YOU</h1>                    <form>                             <span>gps_fixed</span>                        </div>                    </form>                </div>                <div data-map></div>            </div>        </div>    </div>
Enter fullscreen modeExit fullscreen mode

and apply the required styling

.search-container {    background-color: #f0c800;    color: #FFF;    width: 600px;    max-height: 350px;    padding: 25px;    position: absolute;    top: 5%;    left: 10%;    z-index: 5000;}.search-container.hide {    width:0;    height:0;    background-color:transparent;}.search-container.hide h1{ display:none;}.search-container.hide form{ display:none;}
Enter fullscreen modeExit fullscreen mode

A neat, yellow box with a positioning icon should appear in your browser:
Browser location can be used to navigate the map

Let's now add an input field and connect the geocodeing service to translate location names to coordinates.

Add a text input to the yellow overlay:

<form>    <h4>Find by address</h4>    <div>        <input             type="text"                         name="searchAddress"                         placeholder="Search by address"            value="">    </div>    <div>        <div>            <button type="submit">SEARCH</button>            <button type="button">RESET</button>        </div>        <span>gps_fixed</span>    </div></form>
Enter fullscreen modeExit fullscreen mode

Now, let's add an event handler on the form's submit event. Once a user submits the form - we will take the address from thesearchAdress input and pass it to the geocoding service. The service should then respond with the point coordinates of the address, which we will use to update the map's center.

  const form = document.getElementById('storeSearch');  form.addEventListener('submit', function(event) {    event.preventDefault();    const formData = new FormData(form);    const address = formData.get('searchAddress');    searcher.geocode(address, function(response) {      if(response.length){        currentPoint = {          lat: response[0].center.lat,          lng: response[0].center.lng        };        geoCoder.markGeocode(response[0]);      }    });  });
Enter fullscreen modeExit fullscreen mode

You can now put cities or full addresses in the text input. When you submit the form - your map should automatically move to the address you entered and the nearby stores should load.

Final touches

As a final touch - let's add 2 elements: popovers with store details and a list of stores under the map.

Popovers

Update theonMarkersLoaded() method and replace

localMarkers[store.id] = L.marker([store.Location.lat, store.Location.lon])
Enter fullscreen modeExit fullscreen mode

with

      storePopupHtml = `        <div>          ${store.StoreName}        </div>        <div>          ${store.Street}        </div>        <div>          <a           href="https://www.google.com/maps/dir/?api=1&amp;origin=Current+Location&amp;destination=${store.Location.lat},${store.Location.lon}">          <span>location_on</span>Get Directions</a>        </div>`;      localMarkers[store.id] = L.marker([store.Location.lat, store.Location.lon])                                .bindPopup(storePopupHtml);
Enter fullscreen modeExit fullscreen mode

This should provide standard maps popovers, if you'd like to give them some extra style - add this to the CSS file and adjust to your needs:

.leaflet-popup-content-wrapper {    background-color: #4a4a4a;    color: white;    width: 300px;}
Enter fullscreen modeExit fullscreen mode

Store list

Finally - we will put the list of stores that were found under the map.

Add the container in HTML:

<div>    <div>        <div>            Retailers        </div>        <div>            Distance        </div>        <div>            Get Directions        </div>    </div></div>
Enter fullscreen modeExit fullscreen mode

and the following method, which will populate the list:

function renderList(collection = placeCollection) {    if(!collection) {      return;    }    resetList();    for (let i = 0; i < collection.data.length; i++) {      store = collection.data[i].item;      store.distance = (map.distance({ lat: store.Location.lat, lng: store.Location.lon}, currentPoint)/1000).toFixed(2);    }    /* sort by distance */    collection.data.sort((a, b) => {      return parseFloat(a.item.distance) - parseFloat(b.item.distance);    });    for (let i = 0; i < collection.data.length; i++) {      store = collection.data[i].item;      storeTableHtml = `        <div>          <div>              ${ store.StoreName }<br>${ store.Street }          </div>          <div>              ${ store.distance } km          </div>          <div>            <a             href="https://www.google.com/maps/dir/?api=1&amp;origin=Current+Location&amp;destination=${store.Location.lat},${store.Location.lon}">            <span>location_on</span></a>          </div>        </div>      `;  document.getElementById('storeTable').insertAdjacentHTML('beforeend', storeTableHtml);    }  }
Enter fullscreen modeExit fullscreen mode

and the next one, that will clear the list's contents:

function resetList() {    const storeTable = document.getElementById('storeTable');    let lastElement = storeTable.lastElementChild;    while(lastElement && storeTable.childElementCount > 1) {        storeTable.removeChild(lastElement);        lastElement = storeTable.lastElementChild;    }  }
Enter fullscreen modeExit fullscreen mode

and finally - drop this at the end of theonMarkersLoaded() function:

 renderList(collection);
Enter fullscreen modeExit fullscreen mode

That's it! You should now have a fully working webpage, which will display the list of stores you store in Flotiq and will place the store's on a map. Look into ourGit repository for some extra style and let us know in the comments when you build something!
The final result - store locator webpage with data dynamically pulled from Flotiq

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Some comments may only be visible to logged-in visitors.Sign in to view all comments.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

effortless headless CMS

Start building your next project with Flotiq.

Design your data model and let Flotiq generate your own RESTful API, API documentation, and SDKs for popular languages.

More fromflotiq

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp