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:

js filename=static/components/ProductUserCompo.js

const ProductUserCompo = {
  name: "ProductUserCompo",
  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>
            </div>
        </div>
    </div>
</div>
  `,
  mounted() {
    this.$store.dispatch("fetchProducts");
  },
};
export default ProductUserCompo;

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.

Update UI

Let's add the method and button to add the product into the cart.

js filename=static/components/ProductUserCompo.js

const ProductUserCompo = {
  name: "ProductUserCompo",
  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">
                ...
                <button v-if="item.quantity>0" class="btn btn-outline-primary" @click="addToCart(item.id, item.name, item.rpu)">Add to cart</button> <! -- [tl! add:start] -->
                <button v-else class="btn btn-danger" disabled>out of stock</button> <! -- [tl! add:end] -->
            </div>
        </div>
    </div>
</div>
  `,
  methods: { // [tl! add:start]
    async addToCart(id, name, rpu) {
      try {
        const response = await fetch("http://127.0.0.1:5000/add/to/cart", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
          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);
      }
    },
  },  // [tl! add:end]
  mounted() {
    this.$store.dispatch("fetchProducts");
  },
};
export default ProductUserCompo;

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=app.py

...
@main.route('/add/to/cart', methods=['POST'])
@jwt_required()
def add_to_cart():
    data = request.get_json()
    product_exist = Cart.query.filter_by(product_id=int(data['id'])).first()
    product = Product.query.filter_by(id=int(data['id'])).first()
    if product_exist:
        if product.quantity > product_exist.quantity:
            product_exist.quantity += 1
            db.session.commit()
            cart_list = {
                '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
            }
            return jsonify({"message": "added to cart",
                            'resource': cart_list}), 209
        return jsonify({"message": "No more qty available"}), 200
    else:
        if product.quantity > 0:
            cart_item = Cart(product_id=product.id, product_name=product.name,
                             rpu=product.rpu, quantity=1,
                             user_id=current_user.id)
            db.session.add(cart_item)
            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 jsonify({"message": "added to cart",
                            'resource': cart_list}), 201
        else:
            return jsonify({"message": "No more qty available"}), 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.

js filename=static/components/CartCompo.js

const CartCompo = {
  name: "CartCompo",
  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>
    `,
  methods: {
    async increaseQuantity(id) {
      try {
        const response = await fetch(
          "http://127.0.0.1:5000/cart/item/increment/" + id,
          {
            method: "PUT",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          }
        );
        if (response.status === 201) {
          const data = await response.json();
          console.log(data.resource);
          this.$store.commit("updateToCart", data.resource);
        } else {
          const data = await response.json();
        }
      } catch (error) {
        console.error(error);
      }
    },
    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("token")}`,
            },
          }
        );
        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 response = await fetch(
          "http://127.0.0.1:5000/cart/item/remove/" + id,
          {
            method: "DELETE",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${localStorage.getItem("token")}`,
            },
          }
        );
        if (response.status === 200) {
          const data = await response.json();
          this.$store.commit("deleteToCart", data.resource);
          console.log(data.resource);
        } else {
          const data = await response.json();
        }
      } catch (error) {
        console.error(error);
      }
    },
    async payAndConfirm() {
      try {
        const response = await fetch("http://127.0.0.1:5000/cart/items/buy", {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
        });
        if (response.status === 200) {
          const data = await response.json();
          this.$store.commit("setCart", data.resource);
          alert(data.message);
        } else {
          const data = await response.json();
        }
      } catch (error) {
        console.error(error);
      }
    },
  },
  // In your component
  computed: {
    totalAmount() {
      return this.$store.getters.getTotalAmount;
    },
  },
};
export default CartCompo;

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.

js filename=static/components/OrderCompo.js

const OrderCompo = {
  name: "OrderCompo",
  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">
                <i v-for="n in item.rate" :key="n" class="fas fa-star"></i>
            </div>
            <div v-else class="star-rating pointer-on-hover">
                <i v-for="n in 5" :key="n" @click="rate(item.id, n)" class="far fa-star"></i>
            </div>
          </div>
      </div>
      <div v-if="this.$store.state.orders.length == 0" class="col-md-12">
        <h5>No Orders Found</h5>
      </div>
  </div>
</div>
    `,
  mounted() {
    this.$store.dispatch("fetchOrders");
  },
};
export default OrderCompo;

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=app.py

@app.route('/get/orders', methods=['GET'])
@jwt_required()
def get_orders():
    try:
        orders = Order.query.filter_by(user_id=current_user.id).all()
        orders_list = []
        for order in orders:
            cat = {
                'id': order.id,
                'product_name': order.product_name,
                'user_id': order.user_id,
                'quantity': order.quantity,
                'rate': order.rate,
                'total': order.total,
                'image': (base64.b64encode(order.image).decode('utf-8')
                          if order.image else None),
                'order_date': order.order_date.strftime("%Y-%m-%d")
            }
            orders_list.append(cat)
        return jsonify(orders_list), 200
    except Exception as e:
        return jsonify({'error': str(e)})

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.

js filename=static/views/UserApp.js

const UserApp = {
  name: "UserApp",
  template: `
    <div>
    <nav class="navbar navbar-expand-lg navbar-dark bg-success">
        <div class="container">
            ...
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item"> <!-- [tl! add:start] -->
                        <a class="nav-link pointer-on-hover" @click="shareViaWhatsApp" >Refer</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link pointer-on-hover" @click="logout">logout</a>
                    </li> <!-- [tl! add:end] -->
                </ul>
            ...
            </div>
        </div>
    </nav>
    ...
  </div>
    `,
  ...
  methods: {
    ...
    async logout() {
      try {
        const response = await fetch("http://127.0.0.1:5000/logout", {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${localStorage.getItem("token")}`,
          },
        });
        if (response.status === 200) {
          const data = await response.json();
          this.$store.commit("setAuthenticatedUser", "");
          localStorage.removeItem("token");
          if (this.$route.path != "/app") {
            this.$router.push("/app");
          }
        } else {
          const data = await response.json();
          alert(data.message);
        }
      } catch (error) {
        console.error(error);
      }
    },
    shareViaWhatsApp() {
      const message = encodeURIComponent(
        "Check out this amazing app! Join now."
      );
      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");
    },
    ...
  },
  ...
};
export default UserApp;

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