Skip to content

08. User Features

We will now focus on the user features. Users can visit the grocery app to place orders. We must manage product availability, prices, order history, and cart functionality. Additionally, users should be able to provide feedback by rating purchased products.

Before proceeding further, let me clarify that we will not display the products in the same manner as we do for the Admin and Manager. Instead, we will include options to add products to the cart, along with displaying their prices, names, etc. Therefore, we will create a separate component specifically for presenting products to our users.

Lets create the component:


vue filename=static/components/ProductUserCompo.vue
<template>
  <div class="container d-flex justify-content-center mt-2" style="margin-bottom: 100px; overflow-y: scroll;">
    <div class="row gap-2">
      <!-- card -->
      <div v-for="item in this.$store.state.products" :key="item.id"
        class="card shadow p-3 mb-5 bg-body-tertiary rounded" style="width: 18rem;">
        <img :src="'data:image/jpeg;base64,' + item.image" class="card-img-top img-thumbnail" :alt="item.name">
        <div class="card-body">
          <h5 class="card-title">{{ item.name }}</h5>
          <p class="card-text"> Price: &#x20B9; {{ item.rpu }}</p>
          <p class="card-text">{{ item.description }}</p>
          <button v-if="item.quantity > 0" class="btn btn-outline-primary"
            @click="addToCart(item.id, item.name, item.rpu)">Add to cart</button>
          <button v-else class="btn btn-danger" disabled>out of stock</button>
        </div>
        <div v-if="item.avg_rate" class="card-footer">
          <font-awesome-icon v-for="n in item.avg_rate" :key="n" :icon="['fa', 'star']" />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    async addToCart(id, name, rpu) {
      try {
        const response = await fetch('http://127.0.0.1:5000/add/to/cart', {
          method: 'POST',
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + localStorage.getItem('jwt')
          },
          body: JSON.stringify({
            "id": id,
            'name': name,
            'rpu': rpu
          }),
        });
        if (response.status === 201) {
          const data = await response.json();
          console.log(data.resource)
          this.$store.commit('addToCart', data.resource)
        } else if (response.status === 209) {
          const data = await response.json();
          alert(data.message);
          this.$store.commit('updateToCart', data.resource)
        } else if (response.status === 200) {
          const data = await response.json();
          alert(data.message);
        }
        else {
          const data = await response.json();
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    }
  },
  mounted() {
    this.$store.dispatch('fetchProducts')
  }
}
</script>

Now I need to add this fetchProducts into our actions of Vue store also include this component into the Vue router.

First Let's add the fetchProducts into static/store/index.js file:


js filename=static/store/index.js
const store = new Vuex.Store({
    state: {
      products: [],
      managers: [] // [tl! add]
    },
    mutations: {
      ...
      setProducts: (state, products) => {
        state.products = products;
      }     
    }
    actions: {
        // write your code here
      ...
      async fetchProducts({ commit }) {
        try {
          const response = await fetch("http://127.0.0.1:5000/get/products", {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          });
          if (response.status === 200) {
            const data = await response.json();
            console.log(data, "products fetched");
            commit("setProducts", data);
          } else {
            const data = await response.json();
            alert(data.message);
          }
        } catch (error) {
          console.error(error);
        }
      },
      ...
    }
});
export default store;

Now the user can see the products on the screen.

Add to Cart

We will enhance the product display by adding an Add to Cart button for each product, allowing users to easily add products to their shopping cart.

To implement this feature, we'll follow the MVC pattern, starting with the model definition.

Update Database

Let's create a table with name Cart in the database for holing cart items of a user.


python filename=database.py
...
class Cart(db.Model): # [tl! add:start]
    __tablename__ = 'cart'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
    product_name = db.Column(db.String)
    rpu = db.Column(db.Float, nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id')) # [tl! add:end]

We have added a Foreign Key because each cart will belong to user. An User can have multiple entries for each unique item whatever they add into his/her cart but an entry into the Cart table will have unique user. So it is like One-to-Many relation.

The code commits two mutations to the store: addToCart and updateToCart. The addToCart mutation is responsible for adding a new product to the cart, while the updateToCart mutation updates the quantity of an existing product in the cart. If a product is already present in the cart, the quantity is updated accordingly.

Let's implement these two mutations.


js filename=static/store/index.js
const store = new Vuex.Store({
    ...
    mutations: {
      ...
      addToCart: (state, newProduct) => { // [tl! add:start]
        state.cart.push(newProduct);
      },     
      updateToCart: (state, updatedProduct) => {
        const index = state.cart.findIndex((p) => p.id === updatedProduct.id);
        if (index !== -1) {
          // Replace the existing product with the updated one
          state.cart.splice(index, 1, updatedProduct);
        }
      } // [tl! add:end]
    }
    ...
});
export default store;

Note state.cart.splice(index, 1, updatedProduct); Remove the product at the specified index (the 1 indicates removing one item).

Backend Implementation


 python filename=main.py
...
class CartResource(Resource):
    @jwt_required()
    def put(self, id):
        # will pass operation from front for incre or decre
        cart_item = Cart.query.filter_by(id=id).first()
        product = Product.query.filter_by(id=cart_item.product_id).first()
        if product.quantity > cart_item.quantity:
            cart_item.quantity += 1
            db.session.commit()
            cart_list = {
                'id': cart_item.id,
                'product_id': cart_item.product_id,
                'product_name': cart_item.product_name,
                'rpu': cart_item.rpu,
                'quantity': cart_item.quantity,
                'user_id': cart_item.user_id
            }
            return {"message": "added to cart", 'resource': cart_list}, 201
        return {"message": "No more qty available"}, 200

    @jwt_required()
    def delete(self, id):
        cart_item = Cart.query.filter_by(id=id).first()
        db.session.delete(cart_item)
        db.session.commit()
        return {'message': "remove item", "resource": id}, 200
...

From the code above, we can see that if the product is already in the cart, then we will update the quantity. If the product is not in the cart, then we will add the product into the cart.

Checkout

We will create a new component for checkout.


vue filename=static/components/CartCompo.vue
<template>
  <div class="container mt-5">
    <h2 class="text-center mb-4">Shopping Cart</h2>
    <div class="card">
      <div class="card-body">
        <!-- Cart Items -->
        <div v-for="(item, index) in this.$store.state.cart" :key="index" class="mb-3">
          <div class="d-flex justify-content-between align-items-center">
            <h5 class="card-title">{{ item.product_name }}</h5>
            <p class="mb-0">
              <button class="btn btn-outline-secondary btn-sm" @click="decreaseQuantity(item.id)">-</button>
              {{ item.quantity }}
              <button class="btn btn-outline-secondary btn-sm" @click="increaseQuantity(item.id)">+</button>
            </p>
          </div>
          <div class="d-flex justify-content-between align-items-center">
            <p class="mb-0">Price: &#8377; {{ item.rpu }}</p>
            <p class="mb-0">Subtotal: &#8377; {{ item.rpu * item.quantity }} </p>
            <button class="btn btn-danger btn-sm" @click="removeItem(item.id)">Remove</button>
          </div>
        </div>
        <!-- Total Amount -->
        <div class="mt-4">
          <h5>Total Amount: {{ totalAmount }}</h5>
        </div>

        <!-- Pay and Confirm Button -->
        <div class="mt-4 text-center">
          <button v-if="this.$store.state.cart.length > 0" class="btn btn-success" @click="payAndConfirm">Pay and
            Confirm</button>
          <button v-else class="btn btn-success" disabled>Pay and Confirm</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script type="module">
import { increaseQuantity, removeItem, payAndConfirm } from '../services/apiServices';
export default {
  name: 'CartCompo',
  methods: {
    async increaseQuantity(id) {
      try {
        const data = await increaseQuantity(id);
        this.$store.commit("updateToCart", data.resource);
      } catch (error) {
        console.error(error.msg);
      }
    },
    async decreaseQuantity(id) {
      try {
        const response = await fetch(
          "http://127.0.0.1:5000/cart/item/decrement/" + id,
          {
            method: "PUT",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("jwt")}`,
            },
          }
        );
        if (response.status === 201) {
          const data = await response.json();
          console.log(data.resource);
          this.$store.commit("updateToCart", data.resource);
        } else if (response.status === 200) {
          const data = await response.json();
          this.$store.commit("deleteToCart", data.resource);
        } else {
          const data = await response.json();
        }
      } catch (error) {
        console.error(error);
      }
    },
    async removeItem(id) {
      try {
        const data = await removeItem(id);
        this.$store.commit("deleteToCart", data.resource);
      } catch (error) {
        console.error(error.msg);
      }
    },
    async payAndConfirm() {
      try {
        const data = await payAndConfirm();
        this.$store.commit("setCart", data.resource);
      } catch (error) {
        console.error(error.msg);
      }
    }
  },
  // In your component
  computed: {
    totalAmount() {
      return this.$store.getters.getTotalAmount;
    },
  }
}
</script>

The code demonstrates the implementation of three key features: increasing, decreasing, and removing items from the cart. Each feature relies on specific mutations within the Vuex store. Let's update the code in the static/store/index.js file.


js filename=static/store/index.js
const store = new Vuex.Store({
    ...
    mutations: {
      ...
      setCart: (state, cart) => { // [tl! add:start]
        state.cart = cart;
      },      
      updateToCart: (state, updatedProduct) => {
        const index = state.cart.findIndex((p) => p.id === updatedProduct.id);
        if (index !== -1) {
          // Replace the existing product with the updated one
          state.cart.splice(index, 1, updatedProduct);
        }
      },
      // Mutation for deleting a product
      deleteToCart: (state, itemId) => {
        state.cart = state.cart.filter((p) => p.id !== itemId);
      } // [tl! add:end]      
    }
    ...
});
export default store;

After the implementation of the mutations in the store, now I will create routers for the payment and confirmation page.


js filename=static/router/index.js
...
import CartCompo from "../components/CartCompo.js"; // [tl! add]
const router = VueRouter.createRouter({
    history: VueRouter.createWebHistory(),
    routes: [
        ...
       {
          path: "/app/user",
          component: UserApp,
          children: [
            { path: "/app/user", component: ProductUserCompo },
            { path: "/app/user/CartCompo", component: CartCompo }, // [tl! add]
          ],
        },
        ...
    ]
});
export default router;

An alert message will be displayed upon order confirmation.

My Orders

The application should provide users with the ability to view their order history. The order history page should not only display the orders placed by the user but also allow them to rate the products they have purchased.

Database Update

Let's create a table with name Order in the database for storing the orders. The table will have the following structure:


python filename=database.py
...
class Order(db.Model):
    __tablename__ = 'order'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    product_name = db.Column(db.String)
    image = db.Column(db.BLOB)
    rate = db.Column(db.Integer)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    quantity = db.Column(db.Integer, nullable=False)
    total = db.Column(db.Float, nullable=False)
    order_date = db.Column(db.DateTime, nullable=False)

Since a user can have multiple orders so it is like One-to-Many relation. That's why he have added the Foreign Key with user table.

Creating Component

We will create a component to show the orders. This page will show the order history of the user. Also all the products will have a option to rate.


vue filename=static/components/OrderCompo.vue
<template>
  <div class="container mt-5">
    <div class="row item-container">
      <!-- Item 1 -->
      <div v-for="(item, index) in this.$store.state.orders" :key="index" class="col-md-4">
        <div class="item">
          <img :src="'data:image/jpeg;base64,' + item.image" alt="item.product_name" class="product-image">
          <p>Quantity: {{ item.quantity }}</p>
          <p>Price: &#8377; {{ item.total }}</p>
          <button class="btn btn-success buy-again-btn" disabled>{{ item.order_date }}</button>
          <div v-if="item.rate > 0" class="star-rating">
            <FontAwesomeIcon v-for="n in item.rate" :key="n" :icon="['fas', 'star']" />
          </div>
          <div v-else class="star-rating pointer-on-hover">
            <FontAwesomeIcon v-for="n in 5" :key="n" @click="rate(item.id, n)" :icon="['far', 'star']" />
          </div>
        </div>
      </div>
      <div v-if="this.$store.state.orders.length == 0" class="col-md-12">
        <h5>No Orders Found</h5>
      </div>
    </div>
  </div>
</template>

<script type="module">
import { rate } from '../services/apiServices';
export default {
  methods: {
    async rate(id, value) {
      try {
        const data = await rate(id, value);
        this.$store.commit('updateOrder', data.resource)
      } catch (error) {
        console.error(error.msg);
      }
    },
  },
  mounted() {
    this.$store.dispatch('fetchOrders')
  }
}
</script>

As we can see that we have dispatch an action fetchOrders in the store. Let's implement this action.


js filename=static/store/index.js
const store = new Vuex.Store({
    state: {
      ...
      orders: [],// [tl! add]
    }
    mutations: {
      ...
      setOrders: (state, orders) => { // [tl! add:start]
        state.orders = orders;
      }, // [tl! add:end]
    },
    actions: {
      ...
      async fetchOrders({ commit }) {
      try {
        const response = await fetch("http://127.0.0.1:5000/get/orders", {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
        });
        if (response.status === 200) {
          const data = await response.json();
          console.log(data, "products fetched");
          commit("setOrders", data);
        } else {
          const data = await response.json();
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },      
    }
});
export default store;

Backend Implementation

Let's implement this action in the backend.


python filename=main.py
class CartListResource(Resource):
    @jwt_required()
    def get(self):
        cart_items = Cart.query.filter_by(user_id=current_user.id).all()
        cart_list = []
        print(current_user.id)
        for product_exist in cart_items:
            cart = {
                'id': product_exist.id,
                'product_id': product_exist.product_id,
                'product_name': product_exist.product_name,
                'rpu': product_exist.rpu,
                'quantity': product_exist.quantity,
                'user_id': product_exist.user_id
            }
            cart_list.append(cart)
        print(cart_list)
        return cart_list, 200

We also return the rate if the user has already rated the product. Therefore, if a user has already rated a product, they cannot rate or change it again as per the business logic. This is because the rate is immutable and cannot be changed once it has been set.

Refer

We need to implement a feature on the frontend that allows users to refer the application to others. This will involve updating the code in the User Dashboard.


vue filename=static/views/UserDash.vue
<template>
  <DashCompo>
    <template v-slot:menu>
      <li class="nav-item">
        <a class="nav-link pointer-on-hover" @click="orders">Your Orders</a>
      </li>
      <li class="nav-item">
        <a class="nav-link pointer-on-hover" @click="shareViaWhatsApp">Refer</a>
      </li>
    </template>
    <template v-slot:icon>
      <a @click="cart" class="nav-link pointer-on-hover ms-auto position-relative">
        <!-- Badge for cart items -->
        <FontAwesomeIcon :icon="['fas', 'shopping-cart']" style="font-size: 1.5rem; color: white;" />
        <span v-show="this.$store.state.cart.length > 0"
          class="badge bg-danger position-absolute top-0 start-100 translate-middle">
          {{ this.$store.state.cart.length }}
        </span>
      </a>
    </template>
  </DashCompo>
</template>

<script>
import DashCompo from './DashCompo.vue'
export default {
  components: {
    DashCompo
  },
  methods: {
    notifi() {
      if (this.$route.path != "/user/notifications") {
        this.$router.push("/user/notifications");
      }
    },
    cart() {
      if (this.$route.path != "/user/CartCompo") {
        this.$router.push("/user/CartCompo");
      }
    },
    orders() {
      if (this.$route.path != "/user/your/orders") {
        this.$router.push("/user/your/orders");
      }
    },
    shareViaWhatsApp() {
      // Replace 'your_share_message' with the message you want to share
      const message = encodeURIComponent(
        "Check out this amazing app! Join now."
      );

      // Replace 'your_web_url' with the URL of your web application
      const url = encodeURIComponent("http://127.0.0.1:5000/");

      // Construct the WhatsApp share URL
      const whatsappUrl = `https://api.whatsapp.com/send?text=${message}%20${url}`;

      // Open a new window with the WhatsApp share URL
      window.open(whatsappUrl, "_blank");
    }
  },
  mounted() {
    const source = new EventSource("http://127.0.0.1:5000/stream");
    source.addEventListener(
      this.$store.state.authenticatedUser.email,
      (event) => {
        let data = JSON.parse(event.data);
        alert(data.message);
      },
      false
    );
    this.$store.dispatch('fetchCategories');
    this.$store.dispatch('fetchNoti');
    this.$store.dispatch('fetchCartItems');
  }
}
</script>

We have implemented the logout and share features. The share option includes a link for WhatsApp Web, accompanied by a message. Note that the logout route in the backend has already been implemented in previous steps.

Testing it out

Now we can start the app and test all the feature implemented on this page.

Run the app.



python3 app.py

Commit the changes to the local git.



git add .
git commit -m "added admin features"

Continue to start search feature...