Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 14: Display Product Details

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

In this article we will implement the product details page so that users can be taken to that page when clicking on any product view button.

 

 

 

 

Handling Apis In Lumen

Let’s make little updates to the lumen project so go to the lumen project and make the below updates:

  • Update app/Models/Product as shown below:
<?php
namespace App\Models;

use App\Traits\Helpers;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Helpers;

    protected $appends = ["slug", "description_short", "title_short", "is_discount_active", "price_after_discount", "price_after_discount_numeric"];

    public function category()
    {
        return $this->belongsTo(Category::class, 'category_id')->with('features');
    }

    public function user()
    {
        return $this->belongsTo(User::class, 'created_by');
    }

    public function features()
    {
        return $this->hasMany(ProductFeature::class, 'product_id');
    }

    public function gallery()
    {
        return $this->hasMany(ProductGallery::class, 'product_id');
    }

    public function brand()
    {
        return $this->belongsTo(Brand::class, 'brand_id');
    }

    public function getSlugAttribute()
    {
        return self::slugify($this->title);
    }

    public function getTitleShortAttribute()
    {
        return mb_substr($this->title, 0, 73, 'utf-8');
    }

    public function getDescriptionShortAttribute()
    {
        return mb_substr(strip_tags($this->description), 0, 70, 'utf-8');
    }

    public function getIsDiscountActiveAttribute()
    {
        if($this->discount > 0) {
            if($this->discount_start_date && $this->discount_end_date) {
                if($this->discount_start_date <= date("Y-m-d") && $this->discount_end_date >= date("Y-m-d")) {
                    return true;
                }

                return false;
            } else if($this->discount_start_date && !$this->discount_end_date) {
                if($this->discount_start_date <= date("Y-m-d")) {
                    return true;
                }

                return false;
            } else if(!$this->discount_start_date && $this->discount_end_date) {
                if($this->discount_end_date >= date("Y-m-d")) {
                    return true;
                }

                return false;
            }
        }

        return false;
    }

    public function getPriceAfterDiscountAttribute()
    {
        if($this->getIsDiscountActiveAttribute()) {
            return number_format($this->price - ($this->price * ($this->discount / 100)), 1);
        }

        return number_format($this->price, 1);
    }

    public function getPriceAfterDiscountNumericAttribute()
    {
        if($this->getIsDiscountActiveAttribute()) {
            return $this->price - ($this->price * ($this->discount / 100));
        }

        return $this->price;
    }

    public function scopeDiscountWithStartAndEndDates($query)
    {
        return $query->whereNotNull('discount_start_date')
            ->whereNotNull('discount_end_date')
            ->whereDate('discount_start_date', '<=', date('Y-m-d'))
            ->whereDate('discount_end_date', '>=', date('Y-m-d'));
    }

    public function scopeDiscountWithStartDate($query)
    {
        return $query->whereNotNull('discount_start_date')
            ->whereNull('discount_end_date')
            ->whereDate('discount_start_date', '<=', date('Y-m-d'));
    }

    public function scopeDiscountWithEndDate($query)
    {
        return $query->whereNotNull('discount_end_date')
            ->whereNull('discount_start_date')
            ->whereDate('discount_end_date', '>=', date('Y-m-d'));
    }
}
  • Update app/Http/Controllers/ProductsController.php show() method to be like this:
public function show($id)
    {
        $product = Product::with('features', 'gallery', 'brand', 'category')->findOrFail($id);

        return response()->json(['product' => $product], 200);
    }

That’s it. Next let’s head to the Nuxt project and call the show product api.

 

 

Display Product Detail Page

In the Nuxt project go to the api/ directory and create another file named product.js with this code:

api/product.js

const ProductApi = {
  getProduct: (axios, id) => {
    return axios.$get('/api/product/' + id);
  }
}

export {ProductApi};

This file contains just one function getProduct() to retrieve product by id.

As a rule of thumb when working in big projects try to add your (Web Services) in separate files instead of using the controller to add all the heavy logic, this helps create clean and maintainable code.

 

Next let’s imagine to product details page, this is shown in this figure:

Building Ecommerce Website With Lumen Laravel And Nuxtjs - Display Product Details

So we have these sections:

  • Product slider section
  • Product details (including title, pricing, inventory, etc).
  • Product description and specifications.
  • Similar items (if found)

All these data can be obtained from the product details api, so let’s update this page code.

Open pages/p/_id/_slug/index.vue and update with this code:

<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">

                <!-- 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">
                    <label>Quantity:</label>
                    <input type="text" value="1" />
                    <button type="button" class="btn btn-fefault cart" @click="addToCart(this.product.id)">
                      <i class="fa fa-shopping-cart"></i>
                      Add to cart
                    </button>
                  </span>
                                </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";

    export default {
      name: "ProductDetails",
      components: {ProductTemplateMini},
      validate({params}) {
        return /^\d+$/.test(params.id);
      },
      data() {
        return {
          similarProducts: []
        }
      },
      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) {

        }
      }
    }
</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>

In this code the validate() method we saw in the previous article, used to validate route parameters so i check if the route parameters contain and id and this id is numeric otherwise 404 page shown.

Then i used asyncData() in the same way as we did in the previous article to load the product details, in addition to that we use it belong to async await operators. These operators work in the same way as Promise and return a promise so we can resolve or reject, you can learn more about async-wait in this article.

Upon retrieveing product details i composed features (specificates) from the both the product features and category features as shown in this snippet:

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
              });
          });
        }

Finally the asyncData() must return an object with the product and features.

After this i update the head() method responsible for handling page title and mete data to show the category and product titles as the page title and the product description.

Always try to use the head() hook when working with Nuxtjs, to take advantage of the server side rendering to improve SEO experience.

In the mounted() hook i made two things first i re-initialized the bootstrap carousal. Then for the similar products one might ask how we can retrieve similar products so we can display them in the bottom of the page, for the purpose of this tutorial i make a call to search api passing the current product category so we can retrieve similar products from this category and except this product.

Now you can try the product details by running ‘npm run dev‘ and click on the view item button on any single product.

 

Continue to Part15: User Login and Profile

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
1
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments