In this article we will complete the previous part by calling the Apis we have done and connect it with the Nuxt project to display the content in the homepage sections.
Calling The Apis
Go the Nuxt project, the first step is to create an api file that contains the api calls we need just in the same way as we done in the admin. So inside the Nuxt main project online-shop-frontend create a new directory api/ then inside it create a new file home.js and add the below code:
api/home.js
export const HomeApis = { getCategoryMenuTree: (axios) => { return axios.$get('/api/category/menutree'); }, getSliderProducts: (axios) => { return axios.$get('/api/product/slider-products'); }, getLatestProducts: (axios) => { return axios.$get('/api/product/latest-products'); }, getFeaturedCategories: (axios) => { return axios.$get('/api/category/featured-categories'); }, getFeaturedProducts: (axios) => { return axios.$get('/api/product/featured-products'); } }
This is the apis that we have done in the previous part, here we are using axios to call them. The next step is to call these apis in the pages/index.vue
Updating The HomePage
Open pages/index.vue and update it with this code:
pages/index.vue
<template> <div> <Slider v-if="sliderProducts.length" :sliderProducts="sliderProducts"></Slider> <section> <div class="container"> <div class="row"> <div class="col-sm-12 padding-right"> <LatestItems v-if="latestProducts.length" :latestProducts="latestProducts"></LatestItems> <FeaturedCategories v-if="featuredCategories.length" :featured-categories="featuredCategories"></FeaturedCategories> <FeaturedProducts v-if="featuredItems.length" :featuredItems="featuredItems"></FeaturedProducts> </div> </div> </div> </section> </div> </template> <script> import {HomeApis} from '../api/home'; import Slider from '../components/home-components/Slider'; import LatestItems from "../components/home-components/LatestItems"; import FeaturedCategories from "../components/home-components/FeaturedCategories"; import FeaturedProducts from "../components/home-components/FeaturedProducts"; export default { components: { FeaturedProducts, FeaturedCategories, LatestItems, Slider }, data() { return { sliderProducts: [], latestProducts: [], featuredCategories: [], featuredItems: [] } }, head() { return { title: 'Online Shop | Home', meta: [ { hid: 'description', name: 'description', content: 'Online Shop Home Page' } ] } }, mounted() { // retrieve slider products HomeApis.getSliderProducts(this.$axios).then(res => { this.sliderProducts = res.products; }); // retrieve latest items HomeApis.getLatestProducts(this.$axios).then(res => { this.latestProducts = res.products; }); // featured categories HomeApis.getFeaturedCategories(this.$axios).then(res => { this.featuredCategories = res.categories; this.featuredCategories.map(category => { category.products = category.products.slice(0, 4); }); }); // featured products // Try to reduce the main products array into sub arrays, each array with 3 products in order for // the bootstrap carousal to render them properly HomeApis.getFeaturedProducts(this.$axios).then(res => { if(res.products.length == 0) { this.featuredItems = []; } else { const totalProducts = res.products.length; const numCarousalItems = Math.ceil(totalProducts / 3); for(let i = 0; i < numCarousalItems; i++) { this.featuredItems.push({ id: i + '-' + i + '-' + i, products: [] }); } for(let i = 0; i < res.products.length; i++) { const itemIndex = parseInt(i / 3); if(this.featuredItems[itemIndex].products.length == 3) { this.featuredItems[itemIndex + 1].products.push(res.products[i]); } else { this.featuredItems[itemIndex].products.push(res.products[i]); } } } }); } } </script> <style> </style>
As shown in the above code i am calling the apis in the mounted() lifecycle hook, then we pass props to each component that the represent the data coming from the api, for example the <Slider /> component i pass a prop sliderProducts which is an array defined in the data() method and update when the api return a result, the same way applied on the other components.
This is fine and simple but i want to clarify a point about the featured products, if you see in the code that i am reducing the main products array into sub arrays that’s because the <FeaturedProducts /> is a bootstrap caruosal that expects a main array with sub arrays inside it, in this case the caruosal display 3 items every time which means each sub array should contain 3 items at most in the main array.
Another important point to note is the head() hook which provided by Nuxt allows us to add meta data like description, keywords which is important for SEO.
Now let’s update the sub components like <Slider />, <LatestItems /> etc.
Updating The Inner Components
Before we start updating the inner components you should have some dynamic data added from the admin panel so that you can see the results very effectively.
Open components/home-components/Slider.vue and update as shown:
<template> <section id="slider"> <div class="container"> <div class="row"> <div class="col-sm-12"> <div id="slider-carousel" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> <li data-target="#slider-carousel" v-for="(item, index) in this.sliderProducts" :data-slide-to="item.id" :key="item.id" :class="{active: index === 0}"></li> </ol> <div class="carousel-inner"> <div :class="'item ' + (index === 0 ?'active' : '')" v-for="(item, index) in this.sliderProducts" :key="item.id"> <div class="col-sm-6"> <h1>{{ item.title_short }}</h1> <p v-if="item.description_short != ''">{{ item.description_short }}</p> <nuxt-link :to="'/p/' + item.id + '/' + item.slug" class="btn btn-default get">Get it now</nuxt-link> </div> <div class="col-sm-6"> <img v-bind:src="item.gallery[0].image_url.main_slider" class="girl img-responsive" alt="" /> <div class="pricing-badge"><strong>Only</strong> <span>${{ item.price_after_discount }}</span></div> </div> </div> </div> <a href="#slider-carousel" class="left control-carousel hidden-xs" data-slide="prev"> <i class="fa fa-angle-left"></i> </a> <a href="#slider-carousel" class="right control-carousel hidden-xs" data-slide="next"> <i class="fa fa-angle-right"></i> </a> </div> </div> </div> </div> </section> </template> <script> export default { name: "Slider", props: ["sliderProducts"], mounted() { if(this.sliderProducts && this.sliderProducts.length) { // re-initialize carousal $(".carousel").carousel(); } } } </script> <style scoped> </style>
Displaying the data in the slider is a matter of iterating over sliderProducts prop and binding the data like title, slug, description, etc. In the mounted() hook it’s important to re-initialize the carousal because this bootstrap caruosal based on jquery which initializes on the page first load so we must re-initialize it again after updating the slider with the data using $(“.caruosel”).caruosel().
In the same way open components/home-components/LatestItems.vue and update as shown:
<template> <div class="features_items"> <h2 class="title text-center">Latest Items</h2> <div class="col-sm-3" v-for="(item, index) in this.latestProducts" :key="index"> <ProductTemplateNormal :item="item"></ProductTemplateNormal> </div> </div> </template> <script> import ProductTemplateNormal from "../product-templates/ProductTemplateNormal"; export default { name: "LatestItems", components: {ProductTemplateNormal}, props: ["latestProducts"] } </script> <style scoped> </style>
Also open and update components/home-components/FeaturedCategories.vue
<template> <div class="category-tab"> <div class="col-sm-12"> <ul class="nav nav-tabs"> <li v-for="(category, index) in this.featuredCategories" :key="index" :class="{'active': index == 0}"> <a :href="'#' + category.id + '-' + category.id" data-toggle="tab">{{ category.title }}</a> </li> </ul> </div> <div class="tab-content"> <div v-for="(category, index) in this.featuredCategories" :key="index" :class="'tab-pane fade ' + (index == 0 ? 'active in' : '')" :id="category.id + '-' + category.id" > <div class="col-sm-3" v-if="category.products.length" v-for="product in category.products" :key="product.id"> <ProductTemplateSmall :item="product"></ProductTemplateSmall> </div> </div> </div> </div> </template> <script> import ProductTemplateSmall from "../product-templates/ProductTemplateSmall"; export default { name: "FeaturedCategories", components: {ProductTemplateSmall}, props: ["featuredCategories"] } </script>
Update components/home-components/FeaturedProducts.vue
<template> <div class="recommended_items"><!--recommended_items--> <h2 class="title text-center">Featured items</h2> <div id="recommended-item-carousel" class="carousel slide" data-ride="carousel"> <div class="carousel-inner"> <div v-for="(item, index) in this.featuredItems" :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> </template> <script> import ProductTemplateMini from "../product-templates/ProductTemplateMini"; export default { name: "FeaturedProducts", components: {ProductTemplateMini}, props: ["featuredItems"], mounted() { console.info(this.featuredItems); // re-initialize the boostrap carousal $("#recommended-item-carousel").carousel(); } } </script> <style scoped> </style>
In the three components above we are invoking other inner components representing product templates like <ProductTemplateNormal />, <ProductTemplateMini />, <ProductTemplateSmall /> we will see these components below. I am adding these components because every section in the homepage uses different product template, for example the <LatestItems /> component uses template different from the <FeaturedProducts />, every template uses different image size.
Product Templates
In the components/ directory create a new directory named product-templates/ and add these components:
- ProductTemplateNormal.vue
- ProductTemplateSmall.vue
- ProductTemplateMini.vue
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 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> </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 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> </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> export default { name: "ProductTemplateNormal", props: ["item"], methods: { methods: { addToCart(productId) { }, addToWishList(productId) { } } } } </script> <style scoped> </style>
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 :to="'/p/' + item.id + '/' + item.slug">{{ item.title_short }}</nuxt-link></p> <a href="javascript:void(0);" class="btn btn-default add-to-cart" @click="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a> </div> <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div> </div> </div> </template> <script> export default { name: "ProductTemplateSmall", props: ["item"], methods: { addToCart(productId) { } } } </script> <style scoped> </style>
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 :to="'/p/' + item.id + '/' + item.slug">{{ item.title_short }}</nuxt-link></p> <a href="javascript:void(0);" class="btn btn-default add-to-cart" @click="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a> </div> <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div> </div> </div> </template> <script> export default { name: "ProductTemplateMini", props: ["item"], methods: { addToCart(productId) { } } } </script> <style scoped> </style>
These simple components we are just passing a prop item and render them, you may have noticed the addToCart() and addToWishlist() methods, we will implement those methods in future lessons. So for now you can see the result by running ‘npm run dev’ and go to http://localhost:3000.
There is still one thing remaining in this part which is displaying the category tree in the header. To achieve this open components/partials/FrontHeader.vue and update with this code:
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><a href="#"><i class="fa fa-user"></i> Account</a></li> <li><a href="#"><i class="fa fa-star"></i> Wishlist</a></li> <li><a href="#"><i class="fa fa-list"></i> My Orders</a></li> <li><nuxt-link to="/cart"><i class="fa fa-shopping-cart"></i> Cart</nuxt-link></li> <li><nuxt-link to="/login"><i class="fa fa-lock"></i> Login</nuxt-link></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">Home</nuxt-link></li> <li><nuxt-link to="/shop">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">Contact</nuxt-link></li> </ul> </div> </div> <div class="col-sm-3"> <div class="search_box pull-right"> <input type="text" placeholder="Search"/> </div> </div> </div> </div> </div><!--/header-bottom--> </header> </template> <script> import { HomeApis } from '../../api/home'; import CategoryTree from '../../components/partials/CategoryTree'; export default { name: "FrontHeader", components: {CategoryTree}, data() { return { categoriesTree: [] } }, mounted() { HomeApis.getCategoryMenuTree(this.$axios).then(res => { this.categoriesTree = res; }); } } </script> <style scoped> </style>
Here we are calling the <CategoryTree /> component and pass it dataTree prop which represent the array of categories we implemented in the previous part. Now let’s see the <CategoryTree /> component.
components/partials/CategoryTree.vue
<template> <ul role="menu" :class="{ 'sub-menu': !isNestedMenu, 'nested-menu': isNestedMenu === '1' }"> <li v-for="item in this.dataTree" :key="item.id" :class="{ 'has-nested-menu': item.children.length > 0 }"> <a href="#" v-if="item.children.length > 0">{{ item.title }}</a> <i class="fa fa-angle-right" v-if="item.children.length > 0"></i> <nuxt-link :to="'/category' + item.path" v-if="item.children.length === 0">{{ item.title }}</nuxt-link> <CategoryTree v-if="item.children.length > 0" :dataTree="item.children" is-nested-menu="1"></CategoryTree> </li> </ul> </template> <script> export default { name: "CategoryTree", props: ["dataTree", "isNestedMenu"] } </script> <style scoped> </style>
The above code displays the categories in a tree like menu. For this purpose we have to use a different type of components which is recursive components. The <CategoryTree /> component is a recursive component which calls itself as shown above if there are children items for the current item thereby the component calls itself and pass the same dataTree prop which is in this time the item.children.
Continue to Part13: Prepare Shop Page