DEV Community

Cover image for How to build an e-commerce site with Vue.js, Cloudinary and Auth0
Moronfolu Olufunke for Hackmamba

Posted on

How to build an e-commerce site with Vue.js, Cloudinary and Auth0

E-commerce applications are one of the recent fast-growing technological ideas of the century, but this isn’t surprising as its advantages far trump its downsides. Now a store owner can put their goods online and make huge returns while also reducing the stress the buyer has to go through as opposed to traditional shopping.

But how does this apply to a software developer?

Building a robust e-commerce website as a developer shows you must have learnt and honed your skill in dealing with a large amount of data while ensuring that all use cases have been taken care of.
So, what better way to show off your proficiency than by building a mini e-commerce application?

In this tutorial, we will be discussing how to build an e-commerce application in Vue.js using some interesting tools like; Auth0, Cloudinary, Vuex and Paystack.

Tool

  • Auth0 - Auth0 is going to be used to handle the application’s authentication.
  • Cloudinary - This is a video and image management service that will be used for all media processes on the e-commerce website.
  • Vuex - Vuex will take care of our application’s state management.
  • Tachyons - This is the CSS framework to will be using for our e-commerce website.
  • Paystack - Paystack will act as a payment gateway for all purchases made on the e-commerce store.
  • Vue Router - Vue router will be acting as a link that connects the different Vue components to allow rendering and viewing based on the user navigation.

All these tools may seem a little bit overwhelming but they will be demystified as they are put together to build the e-commerce fashion store named Rayo E-store.

Case Study

Rayo E-store is an online e-commerce store for a Fashion retailer in Africa. For a long time, African businesses have had difficulty showing their wares online and even more problems receiving payments for purchased goods. In this tutorial, we will be building a solution to address this problem.

Here is what the final product of the solution will look like…

Prerequisites

This tutorial assumes the reader has the following:

  • Node.js installed on their computer.
  • Familiarity with Vue.js and Vuex.
  • Vue-router
  • Tachyons
  • Paystack
  • Auth0
  • Cloudinary

NB: The best part of this is that creating an account and using Cloudinary and Auth0 in your project is completely free.

Getting Started

To build our e-commerce site, we will be using the Vue CLI to create a Vue app and Cloudinary to manage our media assets for the application. We will also be using VueX and Paystack to handle state management and payment integration, respectively.

Check out the Github repo here, if you would like to jump right into the code.

Now, let’s go ahead and begin building.

Woooo, let's get started!

Create Vue App

Open a terminal on your computer and run the following command.

vue create vue-ecommerce
cd vue-ecommerce
npm run serve
Enter fullscreen mode Exit fullscreen mode

Running npm run serve starts the project on the development server at localhost:8080 in your browser.

Styling

The CSS framework to be used in this project is Tachyons CSS. Install it by running npm install tachyons --save-dev in the terminal.
Afterwards, make it globally available for usage in the project by adding the line below in our main.js:

import tachyons/css/tachyons.css
Enter fullscreen mode Exit fullscreen mode

Router Setup

We will be using Vue Router for the application routing by running this line of code in our terminal:

vue add router
Enter fullscreen mode Exit fullscreen mode

After this, we will register it in our main.js file as shown below:

import router from './router'
 new Vue({
  router,
  render: h => h(App),
 }).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

Cloudinary Setup

For our application’s asset management, if you haven’t already, you will need to create an account with Cloudinary by clicking here. Creating account is completely free.

To use Cloudinary in your project, install the Cloudinary Vue.js SDK with the command below before proceeding to configuring it in your Vue app.

npm install cloudinary-vue 
Enter fullscreen mode Exit fullscreen mode

Configuring Vue App for Cloudinary

  • Create a .env file in the root of your project and add your Cloudinary Cloud name. You can find your cloud name by navigating to your Cloudinary dashboard.
  • Register the cloudname in your .env file like so: VUE_APP_CLOUDINARY_CLOUD_NAME ='****'
  • Register Cloudinary component for global use in your main.js file by adding this:
import Cloudinary from "cloudinary-vue";
 Vue.use(Cloudinary, {
  configuration: { 
   cloudName: "***",
   secure: true 
  }
 });
Enter fullscreen mode Exit fullscreen mode

NB: Ensure you replace all occurrences of “*" with your cloudname.

  • Create a folder named “store” in your media library on Cloudinary and keep all your images for the store there. You can get the images used in the project here.
  • For the creation of our mock API, we will need each image public_id. If you are wondering where to find the public_id on your asset the marked yellow rectangle is the public_id and this is what we will be using to fetch the images for display in our application. how to find the public id in Cloudinary

API Setup

A full-scale e-commerce website will have an API integration where all the application’s products will be fetched but setting that up is beyond the scope of this tutorial. Instead, we will create a mock API that would serve us just well.
Since each products will have some basic values, we will be creating our API to have a structure like this:

  • id
  • name
  • size
  • color
  • price
  • public_id
  • quantityInStock

Navigate back to your project and in your src folder, create a folder named api then proceed to creating a file named productfile.js in the just created api folder.
NB: You can get the data used by downloading via this link

At the end, you should have something that looks like this:

{
 id: 1,
 name:"Ore Sweater",
 size: 'M, XL, XXL',
 color: 'Custom made to what you want',
 price: '15000',
 public_id:"store/store4_rlkr3u",
 quantityInStock: 20,         
}
Enter fullscreen mode Exit fullscreen mode

NB: These are just based on my own preferences, feel free to add more data or create yours.

Vuex Setup

As stated earlier, the applications state management will be handled with Vuex and this will be configured by running the code below in your terminal.

npm install vuex --save 
Enter fullscreen mode Exit fullscreen mode

After successful installation, proceed to creating a folder named store.js in your src folder and add a file named index.js in it.
We also will be registering the plugin globally in your main.js like so:

import store from '@/store/index'
  new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

Auth0 Setup

Auth0 will handle the site’s authorization and authentication as such only authenticated users will be able to view the products catalog and purchase product.
To do this, we will do the following:

  • Create an account on Auth0
  • Install Auth0 SDK by running this command in the terminal: npm install @auth0/auth0-spa-js
  • After creating an account on Auth0, an application will be created for you or you can decide to create a new one yourself.
    • Then proceed to configuring your Logout URLs, Allow Web Origins and Callback URLs with your project’s port which in this case will be http://localhost:8080
  • Create an authentication wrapper to make asynchronous methods of the Auth0 SDK easier to work with. Click here to read the great guide written by the amazing team at Auth0 for setting it up.
  • Proceed to creating a new file called auth_config.json in your root folder and add your clientId and domain like so:
{
  "domain": "xxxx",
  "clientId": "xxxx"
}
Enter fullscreen mode Exit fullscreen mode
  • Afterwards we will have to register the SDK for global usage in our main.js file by adding these:
// Import the Auth0 configuration
import { domain, clientId } from "../auth_config.json";  
// Import the plugin here
import { Auth0Plugin } from "./auth";
// Install the authentication plugin here
  Vue.use(Auth0Plugin, {
    domain,
    clientId,
    onRedirectCallback: appState => {
      router.push(
        appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname
      );
    }
  });
Enter fullscreen mode Exit fullscreen mode

This completes the basic things we need to have ready for our authentication.

Mid Check

What have we gotten so far?

  1. At this stage, we have successfully created a Cloudinary account and configured it in our app.
  2. We have also successfully created the mock API for the project, integrated Vue Router and Vuex and configured all appropriately.
  3. We have also set up the basic scaffold for our user authentication using Auth0. mid check gif

Now, let’s continue…

Creating the Navigation

It is important that users of our application are able to navigate to different sections of the application as seamlessly as possible.
In this section, we will be creating the site’s application by using the Vue router and integrating with Auth0.

At this point, you have to decide on the pages you would like users to see on your application and create the files accordingly in your views folder.
For our e-commerce site, here are the things we want users to be able to see:

  • The home page
  • The products list page
  • The user’s profile page
  • The cart

Now that we have established that, navigate back to your components folder and create a new file named nav.vue where we will create a navigation bar by adding the following code:

<template>
  <div>
    <header class="bg-white black-80 tc pt4 avenir">
      <a href="/">
        <cld-image public-id="store/logo_ja9ugi"></cld-image>
      </a>
      <h1 class="mt2 mb0 baskerville i fw1 f1 mh2">Rayo E-Store</h1>
      <h2 class="mt2 mb0 f6 fw4 ttc tracked i">Your satisfaction is our utmost pleasure...</h2>
      <nav class="bt bb tc mw7 center mt4 flex justify-between flex-wrap">
        <a class="f6 f5-l link bg-animate black-80 hover-bg-lightest-blue dib pv3 ph2 ph4-l" href="/">Home</a>
        <a class="f6 f5-l link bg-animate black-80 hover-bg-washed-red dib pv3 ph2 ph4-l" v-if="$auth.isAuthenticated" href="">
          <router-link class="link black" to="/products">Products</router-link>
        </a>
        <a class="f6 f5-l link bg-animate black-80 hover-bg-light-yellow dib pv3 ph2 ph4-l" v-if="$auth.isAuthenticated">
          <router-link class="link black relative" to="/cart">Cart</router-link>
        </a>
        <a class="f6 f5-l link bg-animate black-80 hover-bg-light-pink dib pv3 ph2 ph4-l pointer" v-if="!$auth.isAuthenticated && !$auth.loading">
          <span id="qsLoginBtn" @click.prevent="login"><i class="fas fa-sign-in-alt mr2"></i>Login</span>
        </a>
        <a class="f6 f5-l link bg-animate black-80 hover-bg-light-green dib pv3 ph2 ph4-l pointer"  v-if="$auth.isAuthenticated">
          <router-link class="link black" to="/profile">Profile</router-link>
        </a>     
        <a class="f6 f5-l link bg-animate black-80 hover-bg-light-pink dib pv3 ph2 ph4-l pointer"  v-if="$auth.isAuthenticated">
          <img :src="$auth.user.picture" class="br-100 w1 h1" alt=""> <span id="qsLogoutBtn" href="#" @click.prevent="logout"> Log out </span>
        </a>
      </nav>
    </header>
  </div>
</template>
<script>
export default {
  name: 'navigation',
  methods: {
    login() {
      this.$auth.loginWithRedirect();
    },
    logout() {
      this.$auth.logout();
      this.$router.push({ path: "/" });
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Updating the App.vue with this component will render a view that looks like this in the browser
The final result in your web browser after this process should produce these:

  • When user is not logged in.
  • When user is logged in How the application looks like

Fetching Products.

Setting up Vuex for fetching products

To update the UI with the product, we will have to fetch the response from the productfile.js. Vuex will be used to handle communication between the mocked API and the UI display.
Hence, we need to modify the store/index.js file with this:

import Vue from "vue";
import Vuex from "vuex";
import shop from '@/api/productfile'
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    products: [],
  },
  getters: {
    availableProducts (state) {
      return state.products
    },
  },
  actions: {
    fetchProducts ({commit}) {
      return new Promise((resolve) => {
        shop.getProducts(products => {
          commit('setProducts', products)
          resolve()
        })
      })
    },
  },
  mutations: {
    setProducts (state,products){
      state.products = products
    },
  }
})
Enter fullscreen mode Exit fullscreen mode

Above, we created a state named products in store/index.js, which has an initial empty array. The purpose of this is to store the responses we will be getting from the api/productfile.js.
The fecthProducts() method in our actions retrieve the responses which on successful fetch, the setProducts method stores the response in the products state.

Updating the UI

To update the UI with the fetched response, we will implement Vue’s mounted()function which will display the results coming in through our computed property - products.

Using Vue directives to iterate through the products array to render the products in our already created productcards.vue component, we should have a ready products list page.
Now, let’s go ahead and add this code to our productcards.vue file.

<template>
  <div>
    <div v-for="product in products" :key="product.id" class="dib">
      <article class="br2 ba dark-gray b--black-10 mv3 w-100 w-90-m w-90-l mw5 center">
        <cld-image :publicId="product.public_id" loadinng="lazy">
          <cld-placeholder 
            type="blur">
          </cld-placeholder>
          <cld-transformation height="250" width="250" crop="fill" />
          <cld-transformation :overlay="{url: 'https://res.cloudinary.com/moerayo/image/upload/v1632557532/store/logo-bgnone_kdje7n.png'}" width="0.4" gravity="south_east"/>
        </cld-image>
        <div class="pa2 ph3-ns pb3-ns bg-washed-red">
          <div class="dt w-100 mt1">
            <div class="">
              <h3 class="f6 mv0">{{product.name}}</h3>
            </div>
            <div class="">
              <p class="f5 mv1">{{product.price}}</p>
            </div>
          </div>
        </div>
      </article>
    </div>
  </div>
</template>
<script>
export default {
  name: 'product',
  computed: {
    products() {
      return this.$store.getters.availableProducts
    }
  },
  mounted() {
    this.$store.dispatch('fetchProducts')
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The code above shows us mapping through the products array in an already created cards component of Tachyons CSS.

NB: You may have noticed a component named cld-image which renders our image. This component is a Cloudinary image component and it has our cloudName attribute, which points to the Cloudinary cloudName already registered in our .env file.
In addition to the cloudName, the component also has the public_id attribute which is returned with our response from the mocked API. This public_id is what cloudinary will use in conjunction with our cloudName to render the images been pointed to at each iteration.

At this stage, we should have a products section that looks like this:

the overview of the products section

Creation of the cart

Our e-commerce is not complete without the functionality that allows the user to add products to cart and check out what they have added to their cart. To do this, we will:

  • Update our store for the carting options.

At the end, we will have the following code in our store/index.js.


//import Vue and dependencies here
export default new Vuex.Store({
  state: {
// products: [],
    cart: []
  },
  getters: {
    /* availableProducts (state) {
      return state.products
    }, */

    cartProducts (state) {
      return state.cart
    },
    cartTotal (state, getters) {
      return getters.cartProducts.reduce((total, product) => total + product.price * product.quantity, 0)
    },
    cartIteming(state){
      return state.cart.length
    }
  },
  actions: {
    fetchProducts ({commit}) {
      // return products
    },
    addProductToCart (context, product) {
      if (product.quantityInStock > 0) {
        const cartItem = context.state.cart.find(item => item.id === product.id)
        if (!cartItem) {
          context.commit('pushProductToCart', product)
        } else {
          context.commit('incrementItemQuantity', cartItem)
        }

      }
    },
    removeProduct (context, product) {
      context.commit('popProductFromCart', product.id)
      context.commit('incrementProductInventory', product)
    },
    removeCartProducts(context){
      context.commit('removeAllProducts')
    }
  },
  mutations: {
    setProducts (state,products){
        //product mutation here
    },
    pushProductToCart (state, product) {
      state.cart.push({
        id: product.id,
        quantity: 1,
        title: product.name,
        price: product.price,
        productprice: product.price,
        newQuantityInStock: product.quantityInStock
      })
    },
    popProductFromCart(state){
      state.cart.pop()
    },
    removeAllProducts(state){
      state.cart = []
    },
    incrementProductInventory (state, product) {
      product.quantityInStock--
    },
    incrementItemQuantity (state, cartItem) {
      const product = state.products.find(product => product.id === cartItem.id)
      cartItem.quantity++
      product.quantityInStock--
      cartItem.productprice = cartItem.quantity * product.price
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

NB: Some codes were commented out as they have been added prior to this stage.

  • Create a new a file and name it cart-checkout.vue in the components folder and add the following code to register the latest change we have made to the store/index.js
<template>
  <div>
    <div class="ph4">
      <h1 class="silver">Your Cart</h1>
      <div class="overflow-auto">
        <table class="f6 w-100 mw8 center" cellspacing="0">
          <thead>
            <tr class="stripe-dark">
            <th class="fw6 tl pa3 bg-white">Product</th>
            <th class="fw6 tl pa3 bg-white">Price</th>
            <th class="fw6 tl pa3 bg-white">Quantity</th>
            <th class="fw6 tl pa3 bg-white">Total</th>
            </tr>
          </thead>
          <tbody class="lh-copy">
            <tr class="stripe-dark" v-for="product in products" :key="product.id">
            <td class="pa3">{{product.title}}</td>
            <td class="pa3">{{product.price}}</td>
            <td class="pa3">
                <input v-model.number="product.quantity" min="1" :max="product.newQuantityInStock" type="number" id="quantity" class="form-control w-75 d-block" >
            </td>
            <td class="pa3">{{product.price *  product.quantity}}</td>
            <td class="pa3">
                <i @click="removeProduct(product)" id="delete-item" class="fas fa-window-close text-danger fa-2x d-block ms-4"></i>
            </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div v-if="!products.length">
        <p class="bg-washed-red pv3 ph2 br2">No item in your cart!</p>
      </div>
      <div class="tl mw8 center w-100">
        <div v-if="products.length>0" class="">
          <p class=" f4"><span class="green fw6 mr2">Total:</span>₦{{total}}</p>
          <button class="bg-washed-red bn br2 pv2 ph3 w-100 w5-ns red di-ns db mr3 link" @click="removeCartProducts()">
            <i class="fas fa-trash"></i> Empty Cart
          </button>
        </div>
        <router-link to="/products" class="link bg-green mt3 pv2 ph3 bn br2 white tc db dib-ns"><i class="fas fa-space-shuttle mr2"></i>Continue Shopping</router-link>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'checkout',
  computed: {
    products () {
      return this.$store.getters.cartProducts
    },
    total () {
      return this.$store.getters.cartTotal
    },
    cartNumber(){
      return this.$store.getters.cartIteming
    }
  },
  methods: {
    removeProduct(product){
      this.$store.dispatch('removeProduct', product)
    },
    removeCartProducts(){
      this.$store.dispatch('removeCartProducts')
    },
  } 
}
</script>
Enter fullscreen mode Exit fullscreen mode

The code above creates a table where the items that have been added to the cart will be displayed while also allowing users modify the quantity of items in their cart as they deem fit.

  • Register the component in the cart.vue file in your views folder.
<template>
  <div>
    <checkout></checkout>
  </div>
</template>
<script>
import checkout from '@/components/cart-checkout.vue';
export default {
  components: {
    checkout
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Updating productscards.vue with the ‘add to cart’ function.

The current UI we have does not in any way give users the ability to add any item to cart but we will change this by updating our productscards.vue file to this:

 <template>
  <div>
    <div v-for="product in products" :key="product.id" class="dib">
      <article class="br2 ba dark-gray b--black-10 mv4 w-100 w-90-m w-90-l mw5 center">
        <cld-image :cloudName="(cloudName)" :publicId="product.public_id" width="400" height="250" crop="scale" />
        <div class="pa2 ph3-ns pb3-ns bg-washed-red">
          <div class="dt w-100 mt1">
            <div class="">
              <h3 class="f6 mv0">{{product.name}}</h3>
            </div>
            <div class="">
              <p class="f5 mv1">{{product.price}}</p>
            </div>
          </div>
          <div>
            <button class="bg-black white bn pa2 w-70 br2 f7 fw2 mv2 pointer" @click="addProductToCart(product)"><i class="fab fa-opencart mr2"></i>Add to cart</button>
          </div>
        </div>
      </article>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The above adds a “Add to Cart” button that allows the user add items to their cart. This button passes the necessary data like the product name, price and quantity we will want displayed in our carts page.

You will need to perform an action before adding items to cart and this can be done by adding this code in the script tag in your productscards.vue file:

methods: {
  addProductToCart(product) {
    this.$store.dispatch('addProductToCart', product)
  }
},
Enter fullscreen mode Exit fullscreen mode

This method is responsible for adding items to the cart.

Your e-commerce website should look like this when you add items to the cart.

overview of carts section

WHOOOT! Yes we did that!

Integrating Paystack for payment

What is an e-commerce site without a way to allow users pay for the items they have added to their cart?
To this end, we will use a payment platform called, Paystack. Paystck allows African businesses get paid by anyone from anywhere in the world.

Setting up Paystack.

If you haven’t before, go to Paystack and create a free account for your business then go ahead and copy the public key and add it the previously created .env file in our root folder like so:

VUE_APP_PAYSTACK_KEY = 'xxx'
Enter fullscreen mode Exit fullscreen mode

The public key can be gotten by navigating to the setting section on the Paystack Dashboard.

where to find Paystack public id on the dashboard

Integrating Paystack component

We will install Paystack in the project like so:

npm install --save vue-paystack`
Enter fullscreen mode Exit fullscreen mode

To use the Paystack component for payment, we will navigate to the file we would like to show the Paystack component and import it there.
In this case, the file we would want to register it in is the cart-checkout.vue file in the components folder.
On addition, the file should look like this:


<template>
  <div>
    <div class="ph4">
      <h1 class="silver">Your Cart</h1>
        <div class="overflow-auto">
          <!-- The code to create the table goes here -->
        </div>
          <!-- code that checks for the producs length in cart goes here -->            
        <div class="tl mw8 center w-100">
          <div v-if="products.length>0" class="">
            <!-- code that calculates the total of carts goes here -->                
            <paystack class="bg-light-yellow gray pv2 ph3 bn br2 mr2 di-ns db w-100 w4-ns mb3" :amount="total*100" :full_name="$auth.user.name" :email="$auth.user.email" :paystackkey="PUBLIC_KEY" :reference="reference" :callback="processPayment" :close="close">
              <i class="fas fa-money-bill-wave mr2"></i>Pay
            </paystack>
          <!--code to remove product from cart goes here-->
        </div>
        <!--router link that allows you access other parts of the applicarion based on your selected preference goes here -->
      </div>
    </div>
  </div>
</template>
<script>
import paystack from 'vue-paystack';
export default {
// name: 'checkout',
  data: () => {
    return {
      amount: null,
      email: null,
      full_name: null,
      PUBLIC_KEY: 'pk_test_b75e40ec3847c3f94d28edbd98492c1687960563'
    };
  },
  components: {
    paystack,
  },
  computed: {
    reference() {
      let text = "";
      let possible =
          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
      for (let i = 0; i < 10; i++)
      text += possible.charAt(Math.floor(Math.random() * possible.length));
      return text;
    },
    //code for calculating the total, 
    // getting cart products
    // and the cart number goes here
  },
  methods: {      
    processPayment: (response) => {
      console.log(response)
    },
    close: () => {
      console.log("You closed checkout page")
    },
    //code for removing products from the product table and removing products from cart goes here
  } 
}
</script>
Enter fullscreen mode Exit fullscreen mode

The result of this should give you a Pay now button which when clicked upon will open a popup that allows the user pay for the items purchased.
NB: Some codes were commented out as they have been added prior to this stage.

The checkout section showing the pay now button

Take a look at what you’ve built and feel free to give yourself a pat on the back!

after building celebratory gift

Conclusion

In this tutorial, we have successfully built an e-commerce application using the following core tools:

  • Vue.js
  • Cloudinary
  • Auth
  • Paystack

We were able to seamlessly use Auth0 for our application users authentication and integrated Cloudinary’s SDK to manage all media assets of the page while handling all transactions with the Paystack payment gateway.

The final product of this tutorial addressed the payment problem of goods faced by African businesses while employing other tools - Cloudinary and Auth0 - to make this experience as seamless as possible.

Here is a link to the live demo hosted on netlify for you to play around with.

Additional Resources

  • Here’s a great guide written by the awesome Auth0 team. It will be of great use if you’d like to know more about using Auth0 with Vue.
  • Cloudinary is a really powerful tool and knowledge of it is something you will want to have under your belt. This guide written by the great Cloudinary team will teach you how to harness it’s power.

Content created for the Hackmamba Jamstack Content Hackathon with Auth0 and Cloudinary.

Discussion (0)