Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 17: Shopping Cart Display

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

In the previous part we have implemented the APIs for the cart functionality and in this part we will complete this by displaying the cart in the pages designated for that.

 

 

 

On the Nuxtjs project go to the api/ folder and create a new file cart.js that will contain the api calls.

api/cart.js

const CartApi = {
  setAuthToken: (axios) => {
    axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
  },
  store: (axios, payload) => {
    CartApi.setAuthToken(axios);
    return axios.$post('/api/cart', payload);
  },
  update: (axios, payload) => {
    CartApi.setAuthToken(axios);
    return axios.$put('/api/cart', payload);
  },
  list: (axios) => {
    CartApi.setAuthToken(axios);
    return axios.$get('/api/cart');
  },
  delete: (axios, id) => {
    CartApi.setAuthToken(axios);
    return axios.$delete('/api/cart/' + id);
  },
  show: (axios, id) => {
    CartApi.setAuthToken(axios);
    return axios.$get('/api/cart/' + id);
  },
  clear: (axios) => {
    CartApi.setAuthToken(axios);
    return axios.$delete('/api/cart/clearAll');
  }
}

export {CartApi};

The file contains calls for the basic operations we have built in the previous part which are (store, update, list, delete, show, clear). Alongside each operation i send the authentication token in the authorization header.

Now let’s create a store for the shopping cart.

 

Shopping Cart Store

Because the shopping have a lot of stuff i created a separate store for that, so create new file cart.js inside store/ directory and add this code:

store/cart.js

import {CartApi} from "../api/cart";

export const state = () => ({
  cart: [],
  validation_errors: [],
  success_message: "",
  error_message: ""
});

export const mutations = {
  addToCart(state, item) {
    state.cart.push(item);
  },
  removeFromCart(state, id) {
    const items = [...state.cart];

    state.cart = items.filter(item => item.id != id);
  },
  clear(state) {
    state.cart = [];
  },
  updateCartItemAmount(state, item) {
    const itemIndex = state.cart.findIndex(i => i.id == item.id);

    state.cart[itemIndex].amount = item.amount;
  },
  updateCartItemTotalPrice(state, item) {
    const itemIndex = state.cart.findIndex(i => i.id == item.id);

    state.cart[itemIndex].total_price_numeric = item.total_price_numeric;
    state.cart[itemIndex].total_price_formatted = item.total_price_formatted;
  },
  updateCartItemAmountTemp(state, item) {
    const itemIndex = state.cart.findIndex(i => i.id == item.id);

    state.cart[itemIndex].amount_temp = item.amount;
  },
  setValidationErrors(state, errors) {
    state.validation_errors = errors;
  },
  setSuccessMessage(state, message) {
    state.success_message = message;
  },
  setErrorMessage(state, message) {
    state.error_message = message;
  },
  setSpinner(state, payload) {
    const itemIndex = state.cart.findIndex(i => i.id == payload.id);

    state.cart[itemIndex].spinner = payload.value;
  }
};

export const actions = {
   store({commit, dispatch, state}, payload) {
     CartApi.store(this.$axios, payload).then(response => {
       if(response.success == 1) {

         response.item.spinner = false;

         commit('addToCart', response.item);

         commit('setSpinner', {id: response.item.id, value: true});

         setTimeout(() => {
           alert(response.message);

           commit('setSpinner', {id: response.item.id, value: false});
         }, 3000);
       }
     }).catch(err => {
       dispatch('handleError', err);
     }).finally(() => {
       setTimeout(() => {
         commit('setErrorMessage', "");
         commit('setSuccessMessage', "");
         commit('setValidationErrors', []);
       }, 3000);
     });
   },
  update({commit, dispatch, state}, payload) {
    CartApi.update(this.$axios, payload).then(response => {
      if(response.success == 1) {

        commit('setSuccessMessage', response.message);
        commit('setSpinner', {id: response.item.id, value: true});

        commit('updateCartItemAmount', response.item);
        commit('updateCartItemTotalPrice', response.item);

        setTimeout(() => {
          commit('setSpinner', {id: response.item.id, value: false});
        }, 3000);
      }
    }).catch(err => {
      dispatch('handleError', err);
    }).finally(() => {
      setTimeout(() => {
        commit('setErrorMessage', "");
        commit('setSuccessMessage', "");
        commit('setValidationErrors', []);
      }, 3000);
    });
  },
  getAll({commit, state}) {
    CartApi.list(this.$axios).then(response => {
        commit('clear');
        response.cart.map(item => {

          item.spinner = false;

          commit('addToCart', item);
        });
    });
  },
  removeCartItem({commit, dispatch}, id) {
    CartApi.delete(this.$axios, id).then(response => {
      if(response.success == 1) {
        commit('setSuccessMessage', response.message);
        commit('removeFromCart', id);
      }
    }).catch(err => {
      dispatch('handleError', err);
    }).finally(() => {
      setTimeout(() => {
        commit('setErrorMessage', "");
        commit('setSuccessMessage', "");
        commit('setValidationErrors', []);
      }, 3000);
    });
  },
  clearCart({commit, dispatch}) {
    CartApi.clear(this.$axios).then(response => {
      if(response.success == 1) {
        commit('setSuccessMessage', response.message);
        commit('clear');
      }
    }).catch(err => {
      dispatch('handleError', err);
    }).finally(() => {
      setTimeout(() => {
        commit('setErrorMessage', "");
        commit('setSuccessMessage', "");
        commit('setValidationErrors', []);
      }, 3000);
    });
  },
  handleError({commit}, err) {
    if(err.response.data) {
      alert(err.response.data.message);
      commit("setErrorMessage", err.response.data.message);
      if(err.response.data.errors) {
        let errors = [];
        for(let key in err.response.data.errors) {
          errors.push(err.response.data.errors[key][0]);
        }

        commit('setValidationErrors', errors);
      }
    }
  }
};

This store responsible for storing user cart data and enables user to add to, remove from and clear cart data. So we have mutations to add to cart, remove from cart, clear all cart data, update cart amount and update cart total price. Also we have five actions that correspond to the five apis we have done previously which are (store(), update(), getAll(), removeCartItem(), clearCart()). 

The next step is to add the code that handles cart addition in the dedicated components but before that i will create a helper file that contains some helper functions.

 

 

Cart Helpers

Create a new file cart.js in the helpers/ directory

helpers/cart.js

export const addToCart = (productId, amount = 1, store, router) => {
  if(!store.state.general.auth.is_logged || !store.state.general.auth.auth_token) {
    router.push('/login');
    return;
  }

  store.dispatch('cart/store', {product_id: productId, amount});

  setTimeout(() => {
    if(store.state.cart.error_message == "") {
      router.push('/cart');
    }
  }, 2000);
}

export const updateCartItemAmount = (product_id, amount, store, router) => {
  if(!store.state.general.auth.is_logged || !store.state.general.auth.auth_token) {
    router.push('/login');
    return;
  }

  store.dispatch('cart/update', {product_id: product_id, amount});
}

export const removeFromCartByProductId = (productId, store, router) => {
  if(confirm("Are you sure?")) {
    if (!store.state.general.auth.is_logged || !store.state.general.auth.auth_token) {
      router.push('/login');
      return;
    }

    const cartItem = store.state.cart.cart.find(item => item.product_id == productId);

    store.dispatch('cart/removeCartItem', cartItem.id);
  }
}

export const removeFromCartByItemId = (id, store, router) => {
  if(confirm("Are you sure?")) {
    if (!store.state.general.auth.is_logged || !store.state.general.auth.auth_token) {
      router.push('/login');
      return;
    }

    store.dispatch('cart/removeCartItem', id);
  }
}

export const isProductInCart = (productId, store) => {
  return store.state.cart.cart.find(item => item.product_id == productId) != undefined;
}

export const clearCart = (store, router) => {
  if(confirm("Are you sure?")) {
    if (!store.state.general.auth.is_logged || !store.state.general.auth.auth_token) {
      router.push('/login');
      return;
    }

    store.dispatch('cart/clearCart');
  }
}

We will need these functions when working on the components instead of repeating the code in each component i extracted these functions and it will be invoked in each component or page. Let’s update those components and pages.

 

Updating Pages & Components

Open and update components/product-templates/ProductTemplateNormal.vue

<template>
    <div class="product-image-wrapper">
      <div class="single-products">
        <div class="productinfo text-center">
          <img v-bind:src="item.gallery[0].image_url.medium" v-bind:alt="item.title" style="width: auto !important;" />
          <h2>${{ item.price_after_discount }}</h2>
          <del v-if="item.is_discount_active">${{ item.price }}</del>
          <p>{{ item.title_short }}</p>
          <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a>
          <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a>
        </div>
        <div class="product-overlay">
          <div class="overlay-content">
            <h2>${{ item.price_after_discount }}</h2>
            <del v-if="item.is_discount_active">${{ item.price }}</del>
            <p>{{ item.title_short }}</p>
            <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a>
            <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a>
          </div>
        </div>
        <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div>
      </div>
      <div class="choose">
        <ul class="nav nav-pills nav-justified">
          <li><a href="javascript:void(0);" @click.prevent="addToWishList(item.id)"><i class="fa fa-plus-square"></i>Add to wishlist</a></li>
          <li><nuxt-link :to="'/p/' + item.id + '/' + item.slug"><i class="fa fa-eye"></i>view item</nuxt-link></li>
        </ul>
      </div>
    </div>
</template>

<script>
    import {addToCart, removeFromCartByProductId, isProductInCart} from '../../helpers/cart';

    export default {
        name: "ProductTemplateNormal",
        props: ["item"],
        data() {
          return {

          }
        },
        methods: {
            addToCart(productId) {
              addToCart(productId, 1, this.$store, this.$router);
            },
            addToWishList(productId) {

            },
            isProductAddedToCart(productId) {
              return isProductInCart(productId, this.$store);
            },
            removeFromCart(productId) {
              removeFromCartByProductId(productId, this.$store, this.$router);
            }
        }
    }
</script>

Open and update components/product-templates/ProductTemplateSmall.vue

<template>
    <div class="product-image-wrapper">
      <div class="single-products">
        <div class="productinfo text-center">
          <img v-bind:src="item.gallery[0].image_url.medium2" v-bind:alt="item.title" style="width: auto !important;" />
          <h2>${{ item.price_after_discount }}</h2>
          <del v-if="item.is_discount_active">${{ item.price }}</del>
          <p><nuxt-link class="item-title" :to="'/p/' + item.id + '/' + item.slug">{{ item.title_short }}</nuxt-link></p>
          <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a>
          <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a>
        </div>
        <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div>

      </div>
    </div>
</template>

<script>
    import {addToCart, removeFromCartByProductId, isProductInCart} from '../../helpers/cart';

    export default {
        name: "ProductTemplateSmall",
        props: ["item"],
        methods: {
          addToCart(productId) {
            addToCart(productId, 1, this.$store, this.$router);
          },
          isProductAddedToCart(productId) {
            return isProductInCart(productId, this.$store);
          },
          removeFromCart(productId) {
            removeFromCartByProductId(productId, this.$store, this.$router);
          }
        }
    }
</script>

<style scoped>
  .item-title {
    color: #696763;
  }
</style>

Update components/product-templates/ProductTemplateMini.vue

<template>
  <div class="product-image-wrapper">
    <div class="single-products">
      <div class="productinfo text-center">
        <img v-bind:src="item.gallery[0].image_url.small" v-bind:alt="item.title" style="width: auto !important;" />
        <h2>${{ item.price_after_discount }}</h2>
        <del v-if="item.is_discount_active">${{ item.price }}</del>
        <p><nuxt-link class="item-title" :to="'/p/' + item.id + '/' + item.slug">{{ item.title_short }}</nuxt-link></p>
        <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a>
        <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a>
      </div>
      <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div>

    </div>
  </div>
</template>

<script>
    import {addToCart, removeFromCartByProductId, isProductInCart} from '../../helpers/cart';

    export default {
        name: "ProductTemplateMini",
        props: ["item"],
        methods: {
          addToCart(productId) {
            addToCart(productId, 1, this.$store, this.$router);
          },
          isProductAddedToCart(productId) {
            return isProductInCart(productId, this.$store);
          },
          removeFromCart(productId) {
            removeFromCartByProductId(productId, this.$store, this.$router);
          }
        }
    }
</script>

<style scoped>
  .item-title {
    color: #696763;
  }
</style>

Update pages/p/_id/_slug/index.vue

<template>
  <section>
    <div class="container">
      <div class="row">

        <div class="col-sm-12 padding-right" v-if="this.product">
          <div class="product-details"><!--product-details-->
            <div class="col-sm-5">
              <div v-if="this.product.gallery.length" id="similar-product" class="carousel slide" data-ride="carousel">

                <div class="alert alert-success" v-if="this.$store.state.cart.success_message != ''">{{ this.$store.state.cart.success_message }}</div>
                <div class="alert alert-danger" v-if="this.$store.state.cart.error_message != ''">{{ this.$store.state.cart.error_message }}</div>

                <div class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.$store.state.cart.validation_errors.length">
                  <ul v-for="(error, index) in this.$store.state.cart.validation_errors" :key="index">
                    <li>{{ error }}</li>
                  </ul>
                </div>

                <!-- Wrapper for slides -->
                <div class="carousel-inner">
                  <div v-for="(imageItem, index) in this.product.gallery" :class="'item ' + (index == 0 ? 'active' : '')">
                    <a href=""><img :src="imageItem.image_url.product_gallery_preview" alt=""></a>
                  </div>

                </div>

                <!-- Controls -->
                <a class="left item-control" href="#similar-product" data-slide="prev">
                  <i class="fa fa-angle-left"></i>
                </a>
                <a class="right item-control" href="#similar-product" data-slide="next">
                  <i class="fa fa-angle-right"></i>
                </a>
              </div>

            </div>
            <div class="col-sm-7">
              <div class="product-information"><!--/product-information-->
                <div class="discount-ribbon-details" v-if="this.product.is_discount_active"><span>{{ this.product.discount }}%</span></div>
                <h2>{{ this.product.title }}</h2>
                <p v-if="this.product.product_code">Item Code: {{ this.product.product_code }}</p>
                <del v-if="this.product.is_discount_active" style="display: block">Price before: ${{ this.product.price }}</del>
                <span>
                                    <span>${{ this.product.price_after_discount }}</span>
                                    <span v-if="this.product.amount > 0 && !isProductAddedToCart(this.product.id)">
                    <label>Quantity:</label>
                    <input type="text" v-model="cart_quantity" min="1" />
                    <button type="button" class="btn btn-fefault cart" @click="addToCart(product.id)">
                      <i class="fa fa-shopping-cart"></i>
                      Add to cart
                    </button>
                  </span>
                  <button type="button" class="btn btn-fefault cart" v-if="isProductAddedToCart(product.id)" @click="removeFromCart(product.id)"><i class="fa fa-trash-o"></i> Remove from cart</button>
                                </span>
                <p><b>Availability:</b> {{ this.product.amount > 0 ? 'In Stock (' + this.product.amount + ' items available)' : 'Not Available' }}</p>
                <p v-if="this.product.brand"><b>Brand:</b> {{ this.product.brand.title }}</p>
                <a href=""><img src="/images/product-details/share.png" class="share img-responsive"  alt="" /></a>
              </div><!--/product-information-->
            </div>
          </div><!--/product-details-->

          <div class="category-tab shop-details-tab"><!--category-tab-->
            <div class="col-sm-12">
              <ul class="nav nav-tabs">
                <li class="active" v-if="this.product.description"><a href="#details" data-toggle="tab">Description</a></li>
                <li :class="(this.product.description ? '' : 'active')" v-if="this.features.length"><a href="#specifications" data-toggle="tab">product specifications</a></li>
              </ul>
            </div>
            <div class="tab-content">
              <div class="tab-pane fade active in" id="details" v-if="this.product.description">
                <div class="tab-body" v-html="this.product.description">
                </div>
              </div>

              <div :class="'tab-pane fade ' + (this.product.description ? '' : 'active in')" v-if="this.features.length" id="specifications" >
                <div class="tab-body">
                  <ul class="specifications">
                    <li v-for="feature in this.features" v-bind:key="feature.id" :title="feature.value">
                      <strong>{{ feature.title }}:</strong> <span v-if="feature.type === 1"> {{feature.value}}</span> <span v-else :style="'background-color: ' + feature.value" class="color-box"></span>
                    </li>
                  </ul>
                </div>
              </div>

            </div>
          </div><!--/category-tab-->

          <div class="recommended_items" v-if="similarProducts.length">
            <h2 class="title text-center">similar items</h2>

            <div id="recommended-item-carousel" class="carousel slide" data-ride="carousel">
              <div class="carousel-inner">

                <div v-for="(item, index) in this.similarProducts" :key="item.id" :class="'item ' + (index == 0 ? 'active' : '') ">
                  <div class="col-sm-4" v-for="product in item.products" :key="product.id">
                    <ProductTemplateMini :item="product"></ProductTemplateMini>
                  </div>
                </div>

              </div>
              <a class="left recommended-item-control" href="#recommended-item-carousel" data-slide="prev">
                <i class="fa fa-angle-left"></i>
              </a>
              <a class="right recommended-item-control" href="#recommended-item-carousel" data-slide="next">
                <i class="fa fa-angle-right"></i>
              </a>
            </div>
          </div>

        </div>
      </div>
    </div>
  </section>
</template>

<script>
    import {ProductApi} from '../../../../api/product';
    import {ShopApi} from '../../../../api/shop';
    import ProductTemplateMini from "../../../../components/product-templates/ProductTemplateMini";
    import {addToCart, removeFromCartByProductId, isProductInCart} from '../../../../helpers/cart';

    export default {
      name: "ProductDetails",
      components: {ProductTemplateMini},
      validate({params}) {
        return /^\d+$/.test(params.id);
      },
      data() {
        return {
          similarProducts: [],
          cart_quantity: 1
        }
      },
      head() {
        return {
          title: 'Online Shop | ' + this.product.category.title + ' | ' + this.product.title,
          meta: [
            {
              hid: 'description',
              name: 'description',
              content: this.product.title
            }
          ]
        }
      },
      async asyncData(context) {
        const features = [];

        const response = await ProductApi.getProduct(context.app.$axios, context.params.id);

        let productFeatures = response.product.features;
        let categoryFeatures = response.product.category.features;

        if(productFeatures.length) {
          productFeatures.forEach((item) => {
              const featureDetail = categoryFeatures.find(f => f.id == item.field_id);
              features.push({
                id: item.field_id,
                title: featureDetail.field_title,
                value: item.field_value,
                type: featureDetail.field_type
              });
          });
        }

        return {
          product: response.product,
          features: features
        }
      },
      mounted() {
        setTimeout(() => {
              // re-initialize carousal
             if($("#similar-product").length) {
               $("#similar-product").carousel();
             }

          // load similar products
          ShopApi.search(this.$axios, "category_id=" + this.product.category_id + "&except=" + this.product.id).then(res => {
            const productsData = res.products.data;

            if(productsData.length == 0) {
              this.similarProducts = [];
            } else {
              const totalProducts = productsData.length;
              const numCarousalItems = Math.ceil(totalProducts / 3);

              for(let i = 0; i < numCarousalItems; i++) {
                this.similarProducts.push({
                  id: i + '-' + i + '-' + i,
                  products: []
                });
              }

              for(let i = 0; i < productsData.length; i++) {
                const itemIndex = parseInt(i / 3);
                if(this.similarProducts[itemIndex].products.length == 3) {
                  this.similarProducts[itemIndex + 1].products.push(productsData[i]);
                } else {
                  this.similarProducts[itemIndex].products.push(productsData[i]);
                }
              }
            }

            // re-initialize the boostrap carousal
            $("#recommended-item-carousel").carousel();
          });

        }, 200);
      },
      methods: {
        addToCart(productId) {

          if(isNaN(parseInt(this.cart_quantity))) {
            alert("Please enter an integer value");
            return;
          }

          if(parseInt(this.cart_quantity) <= 0) {
            alert("Please enter a value greater than or equal 1");
            return;
          }

          addToCart(productId, parseInt(this.cart_quantity), this.$store, this.$router);
        },
        isProductAddedToCart(productId) {
          return isProductInCart(productId, this.$store);
        },
        removeFromCart(productId) {
          removeFromCartByProductId(productId, this.$store, this.$router);
        }
      }
    }
</script>

<style scoped>
  #similar-product .carousel-inner .item img {
    margin-left: 26% !important;
  }

  .tab-pane {
    border: 1px solid #FE980F;
  }

  .tab-body {
    color: #000;
    background: #fff;
    margin-top: 4px !important;
    padding-left: 11px;
  }

  .tab-body ul {
    background: #fff !important;
    list-style: none outside none;
    margin-top: 0 !important;
    border-bottom: none !important;
  }

  .tab-body ul.specifications {
    list-style: auto !important;
  }

  .tab-body ul.specifications li {
    list-style-type: disc !important;
    padding-bottom: 6px;
  }

  .color-box {
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: middle;
  }
</style>

Update pages/cart.vue

<template>
    <div>
      <section id="cart_items">
        <div class="container">
          <h2 class="title text-left">Shopping Cart</h2>

          <div class="alert alert-success" v-if="this.$store.state.cart.success_message != ''">{{ this.$store.state.cart.success_message }}</div>
          <div class="alert alert-danger" v-if="this.$store.state.cart.error_message != ''">{{ this.$store.state.cart.error_message }}</div>

          <div class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.$store.state.cart.validation_errors.length">
            <ul v-for="(error, index) in this.$store.state.cart.validation_errors" :key="index">
              <li>{{ error }}</li>
            </ul>
          </div>

          <div class="table-responsive cart_info">
            <p v-if="this.cart.length == 0" class="text-left alert alert-warning">Your cart is empty! <n-link to="/shop">Add items to the cart</n-link></p>

            <table class="table table-condensed" v-if="this.cart.length > 0">
              <thead>
              <tr class="cart_menu">
                <td class="image" width="15%">Item</td>
                <td class="description" width="35%"></td>
                <td class="price" width="10%">Price</td>
                <td class="quantity" width="15%">Quantity</td>
                <td class="total" width="10%">Total</td>
                <td width="15%"></td>
              </tr>
              </thead>
              <tbody>
              <tr v-for="item in this.cart" v-bind:key="item.id">
                <td class="cart_product">
                  <nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><img :src="item.product.gallery[0].image_url.cart_thumb" v-bind:alt="item.title"></nuxt-link>
                </td>
                <td class="cart_description" width="30%">
                  <h4><nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><span>{{ item.product.title_short }}</span></nuxt-link></h4>
                  <p v-if="item.product.product_code != ''">Product Code: {{item.product.product_code}}</p>
                </td>
                <td class="cart_price">
                  <p>${{ item.product.price_after_discount }}</p>
                </td>
                <td class="cart_quantity">
                  <div class="cart_quantity_button">
                    <a class="cart_quantity_up" href="#" @click.prevent="incrementQuantity(item)"> + </a>
                    <input class="cart_quantity_input" type="text" name="quantity" :value="item.amount_temp" autocomplete="off" size="2" @change="updateQuantity($event, item)">
                    <a class="cart_quantity_down" href="#" @click.prevent="decrementQuantity(item)"> - </a>
                  </div>
                </td>
                <td class="cart_total">
                  <p class="cart_total_price">${{ item.total_price_formatted }}</p>
                </td>
                <td class="cart_delete text-center">
                  <i class="fa fa-spinner fa-spin" v-if="item.spinner"></i>
                  <button type="button" class="btn btn-info" v-if="item.amount != item.amount_temp" v-on:click="saveCartAmount(item)">Save</button>
                  <a class="cart_quantity_delete" href="javascript:void(0);" @click="removeCartItem(item.id)"><i class="fa fa-times"></i></a>
                </td>
              </tr>

              </tbody>
              <tfoot>
                <tr>
                  <td colspan="6">
                    <button type="button" class="btn btn-danger pull-right" @click="clearCart()"><i class="fa fa-trash-o"></i> Clear Cart</button>
                  </td>
                </tr>
              </tfoot>
            </table>
          </div>
        </div>
      </section>

      <section id="do_action" v-if="this.cart.length > 0">
        <div class="container">
          <div class="row">
            <div class="col-sm-6">

            </div>
            <div class="col-sm-6">
              <div class="total_area">
                <ul>
                  <li>Cart Sub Total <span>${{this.getCartTotal()}}</span></li>
                  <li>Shipping Cost <span>Free</span></li>
                  <li>Total <span>${{this.getCartTotal()}}</span></li>
                </ul>
                <a class="btn btn-default check_out" href="">Check Out</a>
              </div>
            </div>
          </div>
        </div>
      </section>
    </div>
</template>

<script>
    import {updateCartItemAmount, removeFromCartByItemId, clearCart} from '../helpers/cart';

    export default {
        name: "Cart",
        middleware: "auth",
        data() {
          return {

          }
        },
      head() {
        return {
          title: 'Online Shop | My Cart',
          meta: [
            {
              hid: 'description',
              name: 'description',
              content: 'My Cart'
            }
          ]
        }
      },
      computed: {
        cart() {
          return this.$store.state.cart.cart;
        }
      },
      methods: {
        incrementQuantity(item) {
            if(item.product.amount >= item.amount_temp) {
              let newAmount = item.amount_temp + 1;

              this.$store.commit("cart/updateCartItemAmountTemp", {id: item.id, amount: newAmount});
            } else {
              return;
            }
        },
        decrementQuantity(item) {
           if(item.amount_temp <= 1) {
             return;
           }

          let newAmount = item.amount_temp - 1;

          this.$store.commit("cart/updateCartItemAmountTemp", {id: item.id, amount: newAmount});
        },
        updateQuantity(event, item) {
          let newAmount = parseInt(event.target.value);

          if(isNaN(newAmount)) {
            alert("Please enter an integer value");
            return;
          }

          if(newAmount == 0) {
            alert("Please enter valid value starting from 1");
            return;
          }

          this.$store.commit("cart/updateCartItemAmountTemp", {id: item.id, amount: newAmount});
        },
        saveCartAmount(item) {
          updateCartItemAmount(item.product_id, item.amount_temp, this.$store, this.$router);
        },
        getCartTotal() {
          let total = 0;
          this.$store.state.cart.cart.map(item => {
            total += item.total_price_numeric;
          });

          return total.toFixed(1);
        },
        removeCartItem(id) {
          removeFromCartByItemId(id, this.$store, this.$router);
        },
        clearCart() {
          clearCart(this.$store, this.$router);
        }
      },
      mounted() {
        if(localStorage.getItem('is_authenticated') === "1" && localStorage.getItem("auth_token") != null) {
          this.$store.dispatch('cart/getAll');
        }
      }
    }
</script>

<style scoped>
  .cart_delete a {
    background: #FE980F;
  }
</style>

As you see the cart page display the user cart data and enables the user to increment or decrement cart item amount, remove items and clear cart and display the total cart price.

Also we need to update the login component to load the user cart data after login so open components/login-component/FrontLogin.vue and update onAfterLogin() method as follows:

onAfterLogin(res) {
            this.$store.dispatch('cart/getAll');
          }

 

Update the header component to show a badge of number of products in cart and clear the cart when the user signout

components/partials/FrontHeader.vue

<template>
  <header id="header"><!--header-->

    <div class="header-middle"><!--header-middle-->
      <div class="container">
        <div class="row">
          <div class="col-sm-4">
            <div class="logo pull-left">
              <nuxt-link to="/"><img src="/images/home/logo.png" alt="" /></nuxt-link>
            </div>
          </div>
          <div class="col-sm-8">
            <div class="shop-menu pull-right">
              <ul class="nav navbar-nav">
                <li><nuxt-link to="/account"><i class="fa fa-user"></i> Account</nuxt-link></li>
                <li><a href="#"><i class="fa fa-star"></i> Wishlist</a></li>
                <li><nuxt-link to="/orders"><i class="fa fa-list"></i> My Orders</nuxt-link></li>
                <li><nuxt-link to="/cart"><i class="fa fa-shopping-cart"></i> Cart <span class="badge badge-danger" v-if="this.cart.length > 0">{{this.cart.length}}</span></nuxt-link></li>
                <li v-if="!this.isLogged"><nuxt-link to="/login"><i class="fa fa-sign-in"></i> Login</nuxt-link></li>
                <li v-if="this.isLogged"><a href="#" v-on:click.prevent="signout()"><i class="fa fa-sign-out"></i> Sign out</a></li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div><!--/header-middle-->

    <div class="header-bottom"><!--header-bottom-->
      <div class="container">
        <div class="row">
          <div class="col-sm-9">
            <div class="navbar-header">
              <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
              </button>
            </div>
            <div class="mainmenu pull-left">
              <ul class="nav navbar-nav collapse navbar-collapse">
                <li><nuxt-link to="/" :class="{active: this.$route.path === '/'}">Home</nuxt-link></li>
                <li><nuxt-link to="/shop" :class="{active: this.$route.path.indexOf('shop') !== -1}">Shop</nuxt-link></li>
                <li class="dropdown">
                  <a href="#">Categories<i class="fa fa-angle-down"></i></a>
                  <CategoryTree v-if="this.categoriesTree.length" :dataTree="categoriesTree"></CategoryTree>
                </li>
                <li><nuxt-link to="/contactus" :class="{active: this.$route.path.indexOf('contactus') !== -1}">Contact</nuxt-link></li>
              </ul>
            </div>
          </div>
          <div class="col-sm-3">
            <form method="get" @submit.prevent="search()">
              <div class="search_box pull-right">
                <input type="text" name="keyword" placeholder="Search" v-model="keyword" />
              </div>
            </form>
          </div>
        </div>
      </div>
    </div><!--/header-bottom-->
  </header>
</template>

<script>
    import CategoryTree from '../../components/partials/CategoryTree';
    export default {
        name: "FrontHeader",
        components: {CategoryTree},
        data() {
          return {
            keyword: ""
          }
        },
        computed: {
          categoriesTree() {
            return this.$store.state.general.categoriesTree;
          },
          isLogged() {
            return this.$store.state.general.auth.is_logged;
          },
          cart() {
            return this.$store.state.cart.cart;
          }
        },
        methods: {
          search() {
            // reset shop filter
            this.$store.dispatch('general/resetShopFilter');

            this.$store.commit('general/setKeyword', this.keyword);

            this.$router.push({ path: "/search", query: {keyword: this.keyword}});

            this.$store.dispatch('general/fetchShopProducts');
          },
          signout() {
            this.$axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
            this.$axios.get('/api/logout').then(response => {
              if(response.data.success) {

                localStorage.removeItem('auth_token');
                localStorage.removeItem('is_authenticated');
                localStorage.removeItem('user_data');

                this.$store.dispatch('general/resetAuthData');

                this.onAfterSignout();

                this.$router.push('/');
              }
            }).catch(err => {
              console.log(err.response);
              this.onAfterSignout();
            });
          },
          onAfterSignout() {
            this.$store.commit('cart/clear');
          }
        },
        mounted() {
          this.$store.dispatch('general/fetchCategoryTree');
        }
    }
</script>

<style scoped>
  .search_box input {
    font-size: 18px;
    color: #424040;
  }

  .badge-danger {
    color: #fff;
    background-color: #dc3545;
  }
</style>

Update components/account-components/AccountSidebar.vue signout() method to be like this:

signout() {
            this.$axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
            this.$axios.get('/api/logout').then(response => {
              if(response.data.success) {

                localStorage.removeItem('auth_token');
                localStorage.removeItem('is_authenticated');
                localStorage.removeItem('user_data');

                this.$store.dispatch('general/resetAuthData');

                this.$store.commit('cart/clear');

                this.$router.push('/');
              }
            }).catch(err => {
              console.log(err.response);
            });
          }

Update layouts/default.vue

<template>
  <div>

    <FrontHeader></FrontHeader>

    <Nuxt />

    <FrontFooter></FrontFooter>
  </div>
</template>

<style>

</style>
<script>
  import FrontHeader from "../components/partials/FrontHeader";
  import FrontFooter from "../components/partials/FrontFooter";

  export default {
    components: {FrontFooter, FrontHeader},
    methods: {
      isAuthenticated() {
        if(process.browser) {
          const user = JSON.parse(localStorage.getItem('user_data'));
          if (localStorage.getItem('is_authenticated') === "1" && localStorage.getItem("auth_token") != null && user.id) {
            this.$store.dispatch('general/storeAuthData', {auth_token: localStorage.getItem("auth_token"), user_data: user});
          } else {
            this.$store.dispatch('general/resetAuthData');

            this.$store.commit('cart/clear');
          }
        }
      },
      checkUserLogin() {
        if(process.browser) {
          // send request to check if the user is logged
          if (localStorage.getItem('auth_token') !== null && localStorage.getItem('auth_token') !== undefined) {
            this.$axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));

            this.$axios.$get('api/check-login').then(response => {
              if (response.success !== 1) {

                localStorage.removeItem('auth_token');
                localStorage.removeItem('is_authenticated');
                localStorage.removeItem('user_data');

                this.$store.dispatch('general/resetAuthData');

                this.$store.commit('cart/clear');

                this.$router.push("/");
              }
            }).catch(err => {
              localStorage.removeItem('auth_token');
              localStorage.removeItem('is_authenticated');
              localStorage.removeItem('user_data');

              this.$store.dispatch('general/resetAuthData');

              this.$store.commit('cart/clear');

              this.$router.push("/");
            });
          } else {
            this.$store.dispatch('general/resetAuthData');
          }
        }
      }
    },
    mounted() {
      this.isAuthenticated();

      const verify = setInterval(this.checkUserLogin, 8000);

      if(!this.$store.state.general.auth.is_logged || !this.$store.state.general.auth.auth_token) {
        clearInterval(verify);
      }

      // fetch shopping cart
      if(localStorage.getItem('is_authenticated') === "1" && localStorage.getItem("auth_token") != null) {
        this.$store.dispatch('cart/getAll');
      }
    }
  }
</script>

That’s fine now experiment the cart functionality by adding products into the cart and see the results.

 

Handling Cart Removal Through Cron Job

To utilize cart removal as we mention this in shopping cart removal in previous part we have to create a cron job to do this. Of course this is so simple in Laravel or Lumen.

So go to the Lumen project and create a new console command in app/Console/Commands folder called ClearCartCommand.php

app/Console/Commands/ClearCartCommand.php

<?php


namespace App\Console\Commands;

use App\Models\Product;
use App\Models\ShoppingCart;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class ClearCartCommand extends Command
{
    protected $signature = "clear:cart";

    protected $description = "Clear user cart data if it exceeds 1 hour";

    public function handle()
    {
        try {

            $userCarts = User::join('shopping_cart', 'users.id', '=', 'shopping_cart.user_id')
                                ->whereRaw("shopping_cart.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)")
                                ->select(DB::raw('shopping_cart.id as shopping_cart_id'), 'product_id', 'amount')
                                ->get();

            if($userCarts->count() == 0) {
                $this->info("No carts found");
                return;
            }

            foreach ($userCarts as $userCart) {
                $cartItemAmount = $userCart->amount;

                $product = Product::find($userCart->product_id);

                if ($product) {
                    $product->increment('amount', $cartItemAmount);
                }

                $cartItem = ShoppingCart::find($userCart->shopping_cart_id);

                $cartItem->delete();
            }

            $this->info("All user carts have been cleared");
        } catch (\Exception $e) {
            $this->error("Error clearing user cart:" . $e->getMessage());
        }
    }
}

Refer to laravel docs to learn about console commands, the above class retrieves users who have items in the shopping cart and it passed 1 hour then removes them and restore the cart amount into the product inventory.

Now register this command in app/Console/Kernel.php

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        Commands\ClearCartCommand::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // run every 15 minutes
        $schedule->command('clear:cart')->cron('*/15 * * * *');
    }
}

I added the command into the $commands property and added it to the schedule() method to run every 15 minutes.

To add this command in linux you can add it to cron tab as follows:

php /your-project-path/artisan schedule:run >> /dev/null 2>&1

 

 

Continue to Part18: Shipping Addresses

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments