Codementor Events

How to create a store locator app using Vuejs, Cosmic JS, and Google Maps.

Published Jul 31, 2018Last updated Jan 27, 2019
How to create a store locator app using Vuejs, Cosmic JS, and Google Maps.

store locator app build using Vue, Cosmic JS, and Google Maps

TL;DR:

Demo

Source code (github)

Cosmic JS backend funcion

Google maps javascript API

Vue js and vuex

Vue-googlemaps

Intro

This little application can be used to allow users to search, and locate your stores. If stores are not relevant to your business, you can it to locate your offices, branches, warehouses… or whatever your business is into. To make this work you need to define a list of your locations that the user can find and locate.

The backend

Using Cosmic JS can saves you a lot of time, because it will allow you to store your data, and also have a REST api ready without any extra coding. For the purpose of this app I used the following features of Cosmic JS:

  • Bucket Objects: to store data on the backend like cities, states, where the stores are available.
  • Bucket Media: to store application images and Google maps icons. This is very convenient to have data and media stored in one place.
  • Web Hosting: to host my application I used Cosmic JS, however you can also use Cosmic JS to deploy your app into other hosting services.


Cosmic JS dashboard — adding cities where your store is available

Starting your vue application

After installing vue-cli you can run the following commands to start building your application

$ npm install -g vue-cli
$ vue init webpack store-locator
$ cd store-locator
$ npm install
$ npm install --save vuex vuetify vue-router vue-resource vue-googlemaps cosmicjs moment
$ npm run dev

The vue application will have two major components:

  • SearchPanel.vue : allow the user to select a city, and get a list of stores
  • MapPanel.vue: display the coresponding map with a marker for each store

You can easily build the application layout with vuetify grid system without the need to write a lot of css.

Adding state management with Vuex and Cosmic JS

import Vue from 'vue'
import Vuex from 'vuex'
import VueResource from 'vue-resource'
import Cosmic from '../api/cosmic'

Vue.use(Vuex)
Vue.use(VueResource)

export default new Vuex.Store({
    state: {
        userLocation: {},
        selectedLocation: {},
        selectedStore: null,
        availableLocations: {},
        stores: []
    },
    getters: {
        ...
    },
    mutations: {
        ...
    },
    actions: {
        ...
    }
})

We need to call Cosmic JS rest api when the application is created to fetch cities which will be used in the search. So in the code below two things are worth mentioning. First we are initializong VueGoogleMaps component with the GOOGLE_API_KEY which is provided from google api console. Second we are calling this.$store.dispatch('fetchCities') to populate the availableLocations on the vuex store file. We can also call this.$store.dispatch('fetchStoreCardImages') that will call Cosmic JS REST api to fetch all images stored within the Media folders.

import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import VueGoogleMaps from 'vue-googlemaps'
import 'vue-googlemaps/dist/vue-googlemaps.css'
import App from './App'
import router from './router'
import store from './store'

Vue.use(Vuetify)

Vue.use(VueGoogleMaps, {
    load: {
        // put your google API key either in the ./config/local.env.js file or just hardcode in the string below
        apiKey: process.env.VUE_APP_GOOGLE_API_KEY || '',
        libraries: ['places'],
        useBetaRenderer: false
    }
})

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
    el: '#app',
    router,
    store,
    components: { App },
    template: '<App/>',
    created () {
        this.$store.dispatch('fetchMapIcons')
        this.$store.dispatch('fetchCities')
        this.$store.dispatch('fetchStoreCardImages')
    }
})

This is the code to call to configure Cosmic JS REST API. You can get the Api read, and write keys after you sign up, and add your Bucket.

const config = {
    bucket: {
        slug: process.env.COSMIC_BUCKET || 'store-locator',
        read_key: process.env.COSMIC_READ_KEY,
        write_key: process.env.COSMIC_WRITE_KEY
    },
    object_type: 'cities'
}

export default config

The Bucket is the root container for all data, files, images for your app.

import Cosmic from 'cosmicjs'
import Config from './config'

export default Cosmic().bucket(Config.bucket)

In the code below as you can see in the fetchCities action we call Cosmic JS api to select all objects of type cities and loop through the result array and create another object to populat the store availableLocations variable which in turn will be used in the SearchPanel component to display a list of available locations.

...
actions: {
        ...
        fetchCities ({commit, dispatch, state}) {
            // will populate the cities from cosmic js REST API
            const params = {
                type_slug: 'cities'
            }
            Cosmic.getObjectsByType(params)
                .then(data => {
                    let locations = {}
                    let country = ''
                    data.objects.forEach(city => {
                        const cityKey = city.slug.toUpperCase()
                        const stateKey = city.metadata.state.slug.toUpperCase()
                        ...
                        locations[stateKey][cityKey].city = city.title
                        locations[stateKey][cityKey].state = city.metadata.state.title
                        locations[stateKey][cityKey].postalCode = city.metadata.postal_code
                        locations[stateKey][cityKey].dataUrl = city.metadata.data_url
                        locations[stateKey][cityKey].country = country
                        locations[stateKey][cityKey].geoPoint = {
                            latitude: city.metadata.lat,
                            longitude: city.metadata.lng
                        }
                    })
                    commit('SET_AVAILABLE_LOCATIONS', locations)
                    // set the initial default location
                    dispatch('updateSelectedLocation', {state: 'FL', city: 'ORLANDO', postalCode: '32821'})
                })
                .catch(err => {
                    console.log(err)
                })
        }
    }

Also in very similar way we can call Cosmic JS REST Api to fetch images and store them in Vuex store using Cosmic.getMedia(...)

...
action: {
  ...
  fetchStoreCardImages ({commit}) {
      // will get the images from Cosmic js media folder and store in vuex
      const params = {
          folder: 'store-card-images'
      }
      Cosmic.getMedia(params)
          .then(data => {
              commit('SET_STORE_CARD_IMAGES', data.media)
          })
          .catch(err => {
              console.log(err)
          })
  }
  ...
}

Adding Google map to your vue application

To display a map, I have used vue-googlemaps component, which is an easy to use Vue component that acts like a wrapper around Google Maps Javascript API. In order to draw a map, you need to provide the mapCenter point which is a geocode of a specific location. For instance Orlando center downtown have the following geopoint {latitude: 30.325974,longitude: -81.660454}. Also to draw a marker on the map that represents a store or a special location, you need a geopoint, a title, and an icon. For the purpose of this application I stored the geolocation of different cities, and stores in Cosmic JS backend.

You may be asking how do I get the geolocations for my home address or work office. There several services that can convert a regular address to a geolocation, and vice versa. But the easiest way, is to open Google maps, and click on any point on the map. Then you will see down the bottom a geopoint of that location.


Google maps showing a geolocation of selected point

Here is the code to draw a map when the application starts

<template>
    <div class="map-panel-container elevation-8 brown lighten-4">
        <googlemaps-map
            ref="map"
            class="map"
            :center.sync="mapCenter"
            :zoom.sync="zoom">

            <googlemaps-marker
                v-for="marker in mapMarkers"
                :key="marker.id"
                :title="marker.title"
                :animation="marker.animation"
                :icon="marker.icon"
                :position="marker.position"
                @click="onMapMarkerClick(marker.id)"
                @mouseover="onMapMarkerMouseOver(marker.id)"
                @mouseout="onMapMarkerMouseOut(marker.id)">
            </googlemaps-marker>

        </googlemaps-map>
    </div>
</template>
<script>
import {mapActions} from 'vuex'
import EventBus from '../eventBus'

export default {
    data () {
        return {
            mapCenter: {lat: 0, lng: 0},
            zoom: 11,
            mapMarkers: null,
            ...
        }
    },
    computed: {
        selectedLocation () {
            return this.$store.getters.selectedLocation
        },
        selectedStore: {
            get () {
                return this.$store.getters.selectedStore
            },
            set (value) {
                this.updateSelectedStore(value)
            }
        },
        stores () {
            return this.$store.getters.stores
        },
        ...
    },
    watch: {
        selectedLocation () {
            this.updateMapCenter(this.selectedLocation)
        },
        selectedStore (newValue, oldValue) {
            this.selectMapMarker(oldValue, false)
            this.selectMapMarker(newValue, true)
        }
    },
    ...
}

In the MapPanel vue component we are using vue-googlemaps to display Google map. All we have to provide is mapCenter, zoom, mapMarkers. We have a couple watchers to update the map. First is the selectedLocation which is triggered whenever the user selects new city from the SearchPanel. We are also watching for stores to populate the map with the markers, which display a little icon for each store within that location. We are also watching for selectedStore to make the corresponding map marker highlighted. This is how we update the map view

methods: {
        ...
        updateMapCenter (location) {
            this.mapMarkers = null
            setTimeout(() => {
                this.mapCenter.lat = location.geoPoint.latitude
                this.mapCenter.lng = location.geoPoint.longitude
                this.zoom = 11
                this.addMapMarkers()
            }, 500)
        },
        addMapMarkers () {
            // go through the stores list and add a map marker for each
            let markers = {}
            for (let i = 0; i < this.stores.length; i++) {
                const marker = {}
                marker.id = this.stores[i].id
                marker.title = this.stores[i].displayName + '\n' + this.stores[i].address.address + '\n' +
                    this.stores[i].phone
                marker.animation = 4
                marker.position = {
                    lat: this.stores[i].geoPoint.latitude,
                    lng: this.stores[i].geoPoint.longitude
                }
                marker.icon = {url: this.mapIcons.defaultIcon, scaledSize: this.mapMarkerIconSize}
                markers[this.stores[i].id] = marker
            }
            this.mapMarkers = markers
        },
        ...
        selectMapMarker (id, isOn) {
            // will make the specified id marker either heilighted or not
            if (this.mapMarkers && this.mapMarkers[id]) {
                const url = isOn ? this.mapIcons.selectedIcon : this.mapIcons.defaultIcon
                const icon = {url: url, scaledSize: this.mapMarkerIconSize}
                this.mapMarkers[id].icon = icon
                if (isOn) {
                    const storeLocation = Object.assign({}, this.mapMarkers[id].position)
                    this.centerOnStore(storeLocation)
                }
            }
        }
    },

And lastly we just need to add a couple events on map. The first event onMapMarkerClick to make the marker icon highlighted, and also make the corresponding store selected on the search pane. onMapMarkerMouseOver to add a little animation to make the icon jump up and down.

Build and deploy

After you build your application and testing it, you need to add .env file to you root folder to have you application configuration, which look like something like this.

GOOGLE_API_KEY=PASTE-YOUR-API-KEY-HERE
COSMIC_BUCKET=store-locator
COSMIC_READ_KEY=PASTE-YOUR-API-KEY-HERE
COSMIC_WRITE_KEY=PASTE-YOUR-API-KEY-HERE
PORT=5000

You also need to add start command to package.json script, which will be used by Cosmic JS hosting to start your application server. Once you update your Api keys, you can just build and deploy to Cosmic JS using the following commands

$ # to run you app on dev mode
$ npm run dev
$ # to test your production server mode
$ npm run start
$ # to build your application before deployment
$ npm run build
$ # commit all changes to github
$ git add .
$ git commit -m 'Before deployment'
$ git push

Now you can just go to Cosmic JS web hosting, and follow the instructions to deploy your application.

Conclusion

Adding Google maps to your application is fun and easy way to enhance user experience. Using Vue.js for this application was also big time saver. However with Cosmic JS I was able to have full stack application with server Rest Api with no additional coding. I think Cosmic JS is an owesome cloud platform, that can boost your development without extra coding.

I would love to hear from you

Lastly, I encourage you to go through the source code, try the application and let me know if you have any questions or comments below.

Keep creating awesome apps.

Cheers.

Discover and read more posts from Mohamed Termoul
get started