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: ₹ {{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: ₹ {{ item.rpu }}</p>
<p class="mb-0">Subtotal: ₹ {{ 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: ₹ {{ 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"