Create a Real-Time Food Ordering Notification Service in Vue.js

Amarachi Iheanacho - May 31 '22 - - Dev Community

Notifications compel users to pay attention to specific pieces of information and encourage user interaction with our content or website.

What we will be building

This post will discuss managing real-time app notifications in Vue.js to create a food ordering notification service. We will subscribe to channels in our database and display notifications when changes occur in the channels.

GitHub URL

https://github.com/Iheanacho-ai/appwrite-food-notification

Prerequisites

To get the most out of this project, we require the following:

  • A basic understanding of CSS, JavaScript, and Vue.js
  • Docker Desktop is installed on the computer, run the docker -v command to verify if we have Docker Desktop installed, if not, install it from the Get Docker documentation
  • An Appwrite instance running on our computer, check out this article to create a local Appwrite instance, we will use Appwrite’s robust database and Realtime service to manage our application

Setting up our Vue.js application

We install the Vue CLI by running these terminal commands to create a new Vue project.

    npm install -g @vue/cli
    # OR
    yarn global add @vue/cli
Enter fullscreen mode Exit fullscreen mode

After installing the Vue CLI, we navigate to our preferred directory and create a new project.

    vue create <name of our project>
Enter fullscreen mode Exit fullscreen mode

We change the directory to the project and start a development server with:

    npm run serve
Enter fullscreen mode Exit fullscreen mode

To see the app, we go to http://localhost:8080/

Installing Dependencies

Installing Tailwind CSS

Tailwind CSS is a "utility-first" CSS framework that allows us to create user interfaces for web applications rapidly.

To install Tailwind CSS in our project, we run these terminal commands.

    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
    npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

These commands create two files in the root directory of our project, tailwind.config.js and postcss.config.js.

In our tailwind.config.js, we add the paths to all our template files with this code below.

    module.exports = {
      purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
      content: [],
      theme: {
        extend: {},
      },
      plugins: [],
    }
Enter fullscreen mode Exit fullscreen mode

Next, we add the tailwind directives in our src/index.css file.

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Installing Vue Router

Routing is the technology used to switch between different pages in our application based on the changes in the current URL.

We run this terminal command to enable our application to use Vue Router.

    npm install vue-router@4

    #or 

    yarn add vue-router@4
Enter fullscreen mode Exit fullscreen mode

Installing Mosha Vue Toastify

Mosha Vue Toastify is a lightweight Vue library that allows us to create super customizable notifications.

We run these commands to install the Mosha Vue Toastify library in our project.

    npm install mosha-vue-toastify

    #or

    yarn add mosha-vue-toastify
Enter fullscreen mode Exit fullscreen mode

Installing Appwrite

Appwrite is an open-source, end-to-end, back-end server solution that allows developers to build applications faster.

To use Appwrite in our Vue application, we install the Appwrite client-side SDK for web applications.

    npm install appwrite
Enter fullscreen mode Exit fullscreen mode

Creating a new Appwrite project

During the creation of the Appwrite instance, we specified what hostname and port we see our console. The default value is localhost:80.

We go to localhost:80 and create a new account to see our console.

On our console, there is a Create Project button. Click on it to start a new project.

Appwrite Console

Our project dashboard appears once we have created the project. At the top of the page, there is a Settings bar. Click it to access the Project ID and API Endpoint.

Appwrite Console Dashboard

We copy the Project ID and API Endpoint, which we need to initialize the Appwrite Web SDK.

Appwrite Console

In our utils/web-init.js file, we initialize our Web SDK with this piece of code.

    import { Appwrite } from 'appwrite';
    export const sdk = new Appwrite();
    sdk
      .setEndpoint('http://localhost/v1') // Replace this with your endpoint
      .setProject('projectID'); // Replace this with your ProjectID
Enter fullscreen mode Exit fullscreen mode

Creating an anonymous user session

Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, they allow us to create an anonymous session to bypass that policy for simple projects.

We create an anonymous user session in our utils.js file.

    import { Appwrite } from 'appwrite';
    export const sdk = new Appwrite();
    sdk
      .setEndpoint('http://localhost/v1') // Replace this with your endpoint
      .setProject('62532ce64d84a58176da'); // Replace this with your ProjectID

    // Create an anonymous user session

    sdk.account.createAnonymousSession().then(
       (response) => {
         console.log(response);
       },
       (error) => {
         console.log(error);
       }
    );
Enter fullscreen mode Exit fullscreen mode

Creating the collection and attributes

Next, we set up our database that will store our order status. In the Appwrite web Console, we click on Database on the left side of the dashboard.

Appwrite Console Dashboard

We create a collection in our database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.

At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can modify the permissions to specify who has access to read or write to our database.

Appwrite Console Permission Page

On the right of the Permissions page, we copy the Collection ID, which we need to perform operations on the collection’s documents.

Next, we go to our Attributes tab to create the properties we want a document to have.

Appwrite Attributes Tab

We create a string attribute of orderStatus.

Appwrite Console Attributes

Creating our food ordering notification application.

Our food notification application will have two pages. One page will place an order and update the order’s status, which could either be accepted, prepared, en route, or delivered. The second page subscribes to the order status and notifies us about any updates to it in real-time.

Creating our Order Page

Our order page will consist of a button to place an order and a select list to update our order status.

We create a views folder in our src folder to create this user interface. This src/views folder will contain a Home.vue file.

We create our order page in our src/views/Home.vue file.

    <template>
      <div class="home rounded overflow-hidden shadow-lg">
        <div class="home-container">
          <button type="button" class="bg-gray-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Place Order</button>
          <h3>Change Order Status to follow your order</h3>
          <select name="order-status" id="order-status" class ="relative w-150 bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
            <option value="Order Placed"  >Order Placed</option>
            <option value="Order Accepted" >Order Accepted</option>
            <option value="Prepared" >Prepared</option>
            <option value="En-route" >En-route</option>
            <option value="Delivered">Delivered</option>
          </select>
        </div>
      </div>
    </template>

    <style>
      #app {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 80vh;
        text-align: center;
      }

      .app-container{
        height: 400px;
      }

      .page-link{
        margin: 10px;
        text-decoration: underline;
      }
    </style>

Enter fullscreen mode Exit fullscreen mode

Routing to our pages

In our src/main.js file, we enable our application to use router to navigate to different pages with this piece of code.

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import './index.css';

    createApp(App).use(router).mount('#app')
Enter fullscreen mode Exit fullscreen mode

Next, we create a router folder in our src folder. Our router folder will contain an index.js file. This file will be responsible for defining what routes lead to specific components.

    // src/router/index.js
    import { createRouter,createWebHistory } from 'vue-router'
    import Home from '@/views/Home'
    import Notification from '@/views/Notifications'
    const routes = [
        {
            path: '/',
            name: 'Home',
            component: Home
        }
    ]
    const router = createRouter({
        history: createWebHistory(process.env.BASE_URL),
        routes
    })
    export default router;
Enter fullscreen mode Exit fullscreen mode

This code block above does the following:

  • Creates a routes array containing the route's path and the component we want the route to lead to.
  • Facilitate routing using Vue createRouter method, which receives an object as a parameter, this object has two fields, a history field and a route field whose value is our route array.

Next, in our App.vue file, we add the links that will link to these pages.

    <template>
      <div class="app-container">
        <router-view ></router-view>
        <router-link class="page-link" to="/">  Home Page</router-link>
      </div>
    </template>

    <script>
      export default {
        name: 'App',
      }
    </script>

    <style>
      #app {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 80vh;
        text-align: center;
      }

      .app-container{
        height: 400px;
      }

      .page-link{
        margin: 10px;
        text-decoration: underline;
      }
    </style>

Enter fullscreen mode Exit fullscreen mode
  • The router-view component represents our component. In the code block above, we specify what position our app is situated.
  • The router-link component acts as an anchor tag by creating links leading to different routes.

To avoid errors resulting from non-multi-level component names, create an ..eslintrc.js file in our views folder.

This views/..eslintrc.js file will contain this code.

    module.exports = {
      rules: {
        'vue/multi-word-component-names': 0,
      },
    }
Enter fullscreen mode Exit fullscreen mode

Go to our http://localhost:8080/ to see our Home page.

Food notifications app home page

Creating our Notifications page

In our src/views folder, we create a Notifications.vue file to display our order’s status.

    <template>
        <div class="notifications rounded overflow-hidden shadow-lg">
            <div class="order-container">
                <h2>Order Placed</h2>
            </div>
        </div>
    </template>

    <style scoped>
        .notifications{
            width: 350px;
            height: 100px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
Enter fullscreen mode Exit fullscreen mode

We add a route to the routes object in the src/router/index.js file to view our notifications page in the browser.

    // src/router/index.js

    const routes = [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            path: '/notification-page',
            name: 'Notification',
            component: Notification
        }
    ]
Enter fullscreen mode Exit fullscreen mode

Here is how our src/router/index.js file looks.

    import { createRouter,createWebHistory } from 'vue-router'
    import Home from '@/views/Home'
    import Notification from '@/views/Notifications'
    const routes = [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            path: '/notification-page',
            name: 'Notification',
            component: Notification
        }
    ]
    const router = createRouter({
        history: createWebHistory(process.env.BASE_URL),
        routes
    })
    export default router;
Enter fullscreen mode Exit fullscreen mode

Next, we add another router-link in our App.vue file as an anchor tag to link to the notifications page.

    <template>
      <div class="app-container">
        <router-view ></router-view>
        <router-link class="page-link" to="/">  Home Page</router-link>

        <!-- link to notification page -->

        <router-link class="page-link" to="/notification-page">Notification Page</router-link>
      </div>
    </template>
Enter fullscreen mode Exit fullscreen mode

To see our notifications page, go to http://localhost:8080/notification-page.

Food notification page

Add interaction with our database

When a user places an order, we create a document in our database that holds the string "Order Placed". Any updates to the order status using the select list will automatically update the document on our Appwrite database.

Our src/views/Home.vue file will be responsible for creating and updating our database document.

Creating our database document

    <script>
      import { sdk } from "../../utils";
      export default {
        name: 'Home',
        data(){
            return{
              documentID: null,
            }
          },
          methods: {
            placeOrder: async function(){
              try {
                let promise = await sdk.database.createDocument('collectionID', 'unique()', {
                  "orderStatus": "Order Placed"
                })

                this.documentID = promise.$id;
                alert("order successfully placed")
              } catch (error) {
                console.log(error)
              }
            },
            updateOrder: async function(e){
              try {
                let orderValue = e.target.value 
                await sdk.database.updateDocument('collectionID', this.documentID, {
                  "orderStatus": orderValue
                })
                alert(orderValue);

              } catch (error) {
                console.log(error)
              }
            }
          }

      }
    </script>
Enter fullscreen mode Exit fullscreen mode

In the code block above, we do the following:

  • Create a documentID variable in our data object to contain our newly created document ID
  • Create a placeOrder function to create a document in our database using Appwrite’s createDocument
  • Create a updateOrder function that we will pass into the select list later in this tutorial where the function collects the value of an option element and uses it to update our document on the database

In our Home.vue file, we pass the placeOrder function to an on-click event listener on our button and our updateDocument function to an on-change event listener on our select list.

    <template>
      <div class="home rounded overflow-hidden shadow-lg">
        <div class="home-container">
          <button type="button" @click = 'placeOrder' class="bg-gray-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Place Order</button>
          <h3>Change Order Status to follow your order</h3>
          <select name="order-status" id="order-status" v-on:change = "updateOrder" class ="relative w-150 bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
            <option value="Order Placed"  >Order Placed</option>
            <option value="Order Accepted" >Order Accepted</option>
            <option value="Prepared" >Prepared</option>
            <option value="En-route" >En-route</option>
            <option value="Delivered">Delivered</option>
          </select>
        </div>
      </div>
    </template>
Enter fullscreen mode Exit fullscreen mode

Here is how our Home.vue file looks.

(https://gist.github.com/Iheanacho-ai/9e47a71adb7fc17b1c5df85c7387dfef)

We place an order to see our document in our database.

Food Notification Page

Food notification Page

Subscribing to updates on the document

We subscribe to our Appwrite database document in our src/views/Notifications.vue.

    <script>
        import { sdk } from "../../utils";

        export default {
            name: 'Notification',
            data(){
                return{
                    orderStatus: ""
                }
            },
            mounted(){
                if(sdk.account.get !== null){
                    try {
                    sdk.subscribe('collections.collectionID.documents', response => {
                        this.orderStatus = response.payload.orderStatus
                    });

                    } catch (error) {
                    console.log(error, 'error')
                    }
                }
            }
        }
    </script>
Enter fullscreen mode Exit fullscreen mode

In the code block above, we do the following:

  • Create an orderStatus variable to hold our response payload
  • Check if we have an active user session before subscribing to our collections channel
  • Subscribe to a channel using Appwrite's subscribe method, which receives two parameters, the channel we subscribe to and a callback function; to understand more about the various channels we can subscribe to, check out Appwrite's Realtime Channels
  • Updates our orderStatus variable

Next, we render our orderStatus variable as text in our Notifications.vue file.

    <template>
        <div class="notifications rounded overflow-hidden shadow-lg">
            <div class="order-container">
                <h2>{{orderStatus}}</h2>
            </div>
        </div>
    </template>
Enter fullscreen mode Exit fullscreen mode

Here is how our Notifications.vue file looks.

(https://gist.github.com/Iheanacho-ai/0f23cf72b2a43be4251f2fb2aa22b7d8)

Creating our Notification

To use the mosha-vue-toastify notification in our application, we import it into our src/views/Notifications.vue

    import { sdk } from "../../utils";
    import { createToast } from 'mosha-vue-toastify';
    import 'mosha-vue-toastify/dist/style.css'
Enter fullscreen mode Exit fullscreen mode

We use the watch object to listen for a variable change in our application. By listening to the orderStatus variable, we can fire off the createToast method every time the variable is updated.

    export default {
        name: 'Notification',
        data(){
            return{
                orderStatus: ""
            }
        },
        watch: {
            orderStatus: function(){
                createToast(this.orderStatus)
            }
        },
        mounted(){
           ...
        }
    }

Enter fullscreen mode Exit fullscreen mode

Here is how our application looks.

Food notification application
Food notification application

Conclusion

This article discussed using Appwrite’s Realtime feature to subscribe to application events.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .