Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 21: Real-time Notifications

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

In this article we will implement an interactive component in our e-commerce app which is displaying real-time notifications when users put an order, next we will display those notifications on the  website admin panel as nice toast messages and in the admin header.

 

 

 

Orders Real-time Notifications With Pusher

We need to display some kind of a real-time notifications using Pusher driver when users put an order so that the admin can be aware when an order sent. In fact there are many options when it comes to sending notifications in laravel such as sockets, pusher, redis. For the purpose of this turorial i will use pusher.

Using and configuring Pusher is very easy, at first go to pusher.com and create a new account. Then login and go to the pusher dashboard and create a new app. Give your app a name and cluster and select Laravel as Backend and Vuejs as Frontend. Next go to the app details and navigate to the App Keys section and grab the pusher configuration which include (app_id, key, secret, cluster).

Now go ahead and open the Lumen project and open .env file and set BROADCAST_DRIVER=pusher and replace the pusher configuration values with the corresponding configurations you got from pusher like this:

BROADCAST_DRIVER=pusher
PUSHER_APP_ID=Your App ID
PUSHER_APP_KEY=Your App Key
PUSHER_APP_SECRET=Your App Secret
PUSHER_APP_CLUSTER=Your App Cluster

Also open config/ directory and create broadcasting.php if not found and add this code:

config/broadcasting.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Broadcaster
    |--------------------------------------------------------------------------
    |
    | This option controls the default broadcaster that will be used by the
    | framework when an event needs to be broadcast. You may set this to
    | any of the connections defined in the "connections" array below.
    |
    | Supported: "pusher", "redis", "log", "null"
    |
    */

    'default' => env('BROADCAST_DRIVER', 'null'),

    /*
    |--------------------------------------------------------------------------
    | Broadcast Connections
    |--------------------------------------------------------------------------
    |
    | Here you may define all of the broadcast connections that will be used
    | to broadcast events to other systems or over websockets. Samples of
    | each available type of connection are provided inside this array.
    |
    */

    'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => true,
            ],
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],

        'log' => [
            'driver' => 'log',
        ],

        'null' => [
            'driver' => 'null',
        ],

    ],

];

Then install pusher composer package:

composer require pusher/pusher-php-server

In order for laravel to broadcast to pusher we have to create an event class. To learn more about laravel events refer to laravel docs. Let’s create an event inside app/Events, i will call my event OrderCreated.

app/Events/OrderCreated.php

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class OrderCreated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $order;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }


    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['notifications'];
    }

    public function broadcastAs()
    {
        return 'orderCreated';
    }

    public function broadcastWith()
    {
        return ["order" => $this->order];
    }
}

As shown laravel events extends from the laravel base Event class and implements ShouldBroadcast interface which means it will fire the event immediatly, you can also queue the event by implementing another interface ShouldQueue.

To specify the data you want to send along with the event you have to implement the broadcastWith() method as you see i send the current order details, also to specify the channel(s) you want to receive your notification implement the broadcastOn() method, in this case i returned an array with just one channel which is “notifications”. The broadcastAs() return the event name to listen to. 

Finally to fire the event we should do that when the user put an order, typically this will be in the OrdersController.php store() method so open app/Http/Controllers/OrdersController.php and in the store() method find this line:

$order = Order::with("user")->find($order->id);

Then add this line after it:

event(new App\Events\OrderCreated($order));

The event() helper function fire the event using the appropriate driver in this case pusher, another way to fire the event using the Event facade as Event::fire(new App\Events\OrderCreated($order)).

 

Listening To Pusher Events

To listen to the pusher event, this will be in our Nuxt project in the admin panel using some kind of javascript code, luckly pusher provides the pusher javascript SDK to listen to pusher events.

At first install this npm package:

npm install vue-toastr

We will that package to display a nice toast messages when event fired. Next open admin/nuxt.config.js and update it like so:

export default {
  /*
  ** Nuxt rendering mode
  ** See https://nuxtjs.org/api/configuration-mode
  */
  mode: 'spa',
  /*
  ** Nuxt target
  ** See https://nuxtjs.org/api/configuration-target
  */
  target: 'server',
  /*
  ** Headers of the page
  ** See https://nuxtjs.org/api/configuration-head
  */
  head: {
    title: 'online shop dashboard',
    bodyAttrs: {
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'online shop dashboard' }
    ],
    link: [
      { rel: 'stylesheet', type: 'text/css', href: '/css/font-face.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/vendor/font-awesome-4.7/css/font-awesome.min.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/vendor/font-awesome-5/css/fontawesome-all.min.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/vendor/mdi-font/css/material-design-iconic-font.min.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/vendor/bootstrap-4.1/bootstrap.min.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/vendor/css-hamburgers/hamburgers.min.css' },
      { rel: 'stylesheet', type: 'text/css', href: '/css/theme.css' }
    ],
    script: [
      {
        src: '/vendor/jquery-3.2.1.min.js',
        type: 'text/javascript'
      },
      {
        src: '/vendor/bootstrap-4.1/popper.min.js',
        type: 'text/javascript'
      },
      {
        src: '/vendor/bootstrap-4.1/bootstrap.min.js',
        type: 'text/javascript'
      },
      {
        src: 'https://js.pusher.com/7.0/pusher.min.js',
        type: 'text/javascript'
      }
    ]
  },
  /*
  ** Global CSS
  */
  css: [
    'quill/dist/quill.core.css',
    'quill/dist/quill.snow.css',
    'quill/dist/quill.bubble.css'
  ],
  /*
  ** Plugins to load before mounting the App
  ** https://nuxtjs.org/guide/plugins
  */
  plugins: [
  ],
  /*
  ** Auto import components
  ** See https://nuxtjs.org/api/configuration-components
  */
  components: true,
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
  ],
  /*
  ** Nuxt.js modules
  */
  modules: [
    // Doc: https://bootstrap-vue.js.org
    // 'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios'
  ],
  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {
    baseURL: 'http://localhost/online-shop-backend/public/'
  },
  /*
  ** Build configuration
  ** See https://nuxtjs.org/api/configuration-build/
  */
  build: {

  },
  srcDir: __dirname,
  buildDir: '.nuxt/admin',
  env: {
    PUSHER_APP_KEY: "your pusher app key"
  }
}

From above i made two updates, the first is the pusher javascript sdk in the script element bove, and the second is the PUSHER_APP_KEY env var.

To facilitate dealing easily with pusher i will create a Nuxt plugin that return the pusher instance so we access it in our components as this.$pusher(). So in admin/plugins create a new javascript file called pusher.js as add this code:

admin/plugins/pusher.js

export default ({app}, inject) => {
  inject('pusher', () => {
    // Pusher.logToConsole = true;  // uncomment this line to show pusher log in the browser console
    return new Pusher(process.env.PUSHER_APP_KEY, {
      cluster: 'eu'   // change your cluster here as per your app
    });
  });
}

Nuxtjs custom plugins return a function that will be injected into The Vue instance, in our case here i called inject(pusher) so we can refer to the pusher in any component or page using this.$pusher().

In the same way let’s add a plugin load vue-toastr.

admin/plugins/toastr.js

import Vue from 'vue';
import VueToastr from "vue-toastr";

Vue.use(VueToastr);

To make Nuxtjs recognize these plugins we must update nuxt.config.js and these plugins to the plugin element like so:

admin/nuxt.config.js

plugins: [
    '~/plugins/pusher.js',
    '~/plugins/toastr.js'
  ]

After those plugins is setup let’s decide how and where to display the real-time notifications. We need to display our notifications as toastr messages for all kinds of orders which include the pending, successful and cancelled for example if a particular user make a successful payment we will display a success toast message.

Also in the top header bell icon we want to display the notifications for pending orders only so that the user can decide and either cancel or verify them.

Now open admin/components/partials/AdminHeader.vue and update with this code:

<template>
    <header class="header-desktop">
      <div class="section__content section__content--p30">
        <div class="container-fluid">
          <div class="header-wrap" style="float: right">
            
            <div class="header-button">
              <div class="noti-wrap">
                <div class="noti__item js-item-menu">
                  <i class="zmdi zmdi-notifications" :title="this.countNotifications > 0 ? this.countNotifications + ' pending orders' : ''"></i>
                  <span class="quantity" v-if="this.countNotifications > 0">{{ this.countNotifications }}</span>
                  <div class="notifi-dropdown js-dropdown">
                    <div class="notifi__title">
                      <p>Latest top {{ this.notifications.length }} Pending Orders</p>
                    </div>
                    <div class="notifi__item" v-for="notification in this.notifications" :key="notification.id">
                      <div class="bg-c3 img-cir img-40">
                        <i class="zmdi zmdi-shopping-cart"></i>
                      </div>
                      <div class="content">
                        <p>{{ notification.status_message }} by {{ notification.user.name  + " (" + notification.user.email + ")" }}</p>
                        <span class="date">{{ notification.created_at_formatted }}</span>
                      </div>
                    </div>
                    <div class="notifi__footer" v-if="this.countNotifications > 0">
                      <nuxt-link to="/orders">All orders</nuxt-link>
                      <nuxt-link to="/orders/pending">Pending orders</nuxt-link>
                    </div>
                  </div>
                </div>
              </div>
              <div class="account-wrap">
                <div class="account-item clearfix js-item-menu">
                  <div class="image">
                    <img src="images/icon/avatar-01.jpg" alt="John Doe">
                  </div>
                  <div class="content">
                    <a class="js-acc-btn" href="#">{{ getUserData().name }}</a>
                  </div>
                  <div class="account-dropdown js-dropdown">
                    <div class="info clearfix">
                      <div class="image">
                        <a href="#">
                          <img src="images/icon/avatar-01.jpg" alt="John Doe">
                        </a>
                      </div>
                      <div class="content">
                        <h5 class="name">
                          <a href="#">{{ getUserData().name }}</a>
                        </h5>
                        <span class="email">{{ getUserData().email }}</span>
                      </div>
                    </div>
                    <div class="account-dropdown__body">
                      <div class="account-dropdown__item">
                        <a href="#">
                          <i class="zmdi zmdi-account"></i>Account</a>
                      </div>
                    </div>
                    <div class="account-dropdown__footer">
                      <a href="#" v-on:click.prevent="logout()">
                        <i class="zmdi zmdi-power"></i>Logout</a>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <audio hidden id="notification_alert">
          <source src="/audio/notification_bell.mp3" type="audio/mpeg">
        </audio>
      </div>
    </header>
</template>

<script>
    import {OrdersApi} from "../../api/orders";

    export default {
        name: "admin-header",
        data() {
          return {
            notifications: [],
            countNotifications: 0
          }
        },
        methods: {
          getUserData() {
            return JSON.parse(localStorage.getItem("user_data"));
          },
          logout() {
            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.$router.push('/login');
              }
            }).catch(err => {
              console.log(err.response);
            });
          },
          reinitializeNotifDrop() {
            try {
              var menu = $('.js-item-menu');
              var sub_menu_is_showed = -1;

              for (var i = 0; i < menu.length; i++) {
                $(menu[i]).on('click', function (e) {
                  e.preventDefault();
                  $('.js-right-sidebar').removeClass("show-sidebar");
                  if (jQuery.inArray(this, menu) == sub_menu_is_showed) {
                    $(this).toggleClass('show-dropdown');
                    sub_menu_is_showed = -1;
                  }
                  else {
                    for (var i = 0; i < menu.length; i++) {
                      $(menu[i]).removeClass("show-dropdown");
                    }
                    $(this).toggleClass('show-dropdown');
                    sub_menu_is_showed = jQuery.inArray(this, menu);
                  }
                });
              }
              $(".js-item-menu, .js-dropdown").click(function (event) {
                event.stopPropagation();
              });

              $("body,html").on("click", function () {
                for (var i = 0; i < menu.length; i++) {
                  menu[i].classList.remove("show-dropdown");
                }
                sub_menu_is_showed = -1;
              });

            } catch (error) {
              console.log(error);
            }
          },
          playNotifAlert() {
            document.getElementById("notification_alert").play();
          }
        },
        created() {
          var channel = this.$pusher().subscribe('notifications');
          channel.bind('orderCreated', (data) => {
            const order = data.order;

            this.playNotifAlert();

            if(order.status === 'pending') {
              this.$toastr.i(`New pending order added by ` + order.user.name, `order Id ${order.id}`);

              this.notifications.unshift(order);
              this.countNotifications++;
              this.reinitializeNotifDrop();
            } else {
              if(order.status === 'success') {
                this.$toastr.s("New successful order payment by " + order.user.name, `order Id ${order.id}`);
              } else {
                this.$toastr.w("New cancelled order payment by " + order.user.name, `order Id ${order.id}`);
              }
            }
          });
        },
        mounted() {
          OrdersApi.getLatestPending(this.$axios).then(response => {
            this.notifications = response.topOrders;
            this.countNotifications = response.countAllPending;
          });
        }
    }
</script>

<style scoped>
  .notifi__item {
    padding-top: 5px !important;
    padding-bottom: 5px !important;
  }

  .notifi__item .content p {
    line-height: 1.5;
    font-size: 13px;
  }

  .mess__title, .email__title, .notifi__title {
    padding: 11px;
  }

  .mess__footer a, .email__footer a, .notifi__footer a {
    padding: 9px 0;
  }
</style>

 

In the code above in the data() method there two items which are the notifications array and countNotifications. Next in the created() hook, this is the place i listen to pusher events, as you see i am using this.$pusher() because it’s injected as a plugin into the Vue app instance.

Next i subscribed to channel and i passed the channel name which “notifications“, the same name we set in the previous part in Lumen, then i bind the channel to the event name using channel.bind(‘orderCreated’). Then inside the bind callback i can access the event data in this case the order using data.order.

Then i invoke a helper method this.playNotifAlert(), to play a sound alert when notification sent, To play sound i created an html <audio/> element and give it an id so i can reference it using document.getElementById().play(), you can download a sample notification sound from this website

After that i made a check for the order status, if the status is pending i display an info toastr message using this.$toastr.i(message), then i prepend the notifications array using this.notifications.prepend(order), increase the notifications count and re-initialize the notifications dropdown using another helper method this.reinitializeNotifDrop(),.

If the order status is cancelled or successful then is display a toastr message only using this.$toastr.s(message) for success and this.$toastr.w(message) for cancelled.

Then i the template i displayed the notifications count close to the bell icon and using v-for to loop and display the notifications array.

Now to try this you have to checkout an order from the website and make a pay on delivery payment or successful payment through paypal so you can see the notifications.

In case the notifications not showing check your pusher configurations and you add them correctly and also you can uncomment this line in the pusher plugin above Pusher.logToConsole=true;

 

 

What’s Missing?

The notifications work well and display in real-time but imagine if you refresh the page and when you login the admin panel for the first time all notifications will be lost. We can fix that by fetching the pending the orders in the mounted hook.

So update the mounted() hook to look like this:

admin/components/partials/AdminHeader.vue

mounted() {
          OrdersApi.getLatestPending(this.$axios).then(response => {
            this.notifications = response.topOrders;
            this.countNotifications = response.countAllPending;
          });
        }

Now when you login into the admin panel for the first time the system fetches the latest pending orders using OrderApi.getLatestPending() method and the real-time notifications will as usual and will update the count and notifications array.

 

 

Continue To Part 22: Wishlist

0 0 vote
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
1

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments