Skip to content

06. More Features

Now we will follow the same cycle of steps to implement rest of the features of application. Since we are creating grocery app, so we need to add add to cart, order, cart, out of stock, checkout etc operations.

So far we have not implement the UI for showing the products to the admin, manager and user. So let's add the UI for showing the products to the admin, manager and user.

Product UI Component For Admin and Manager

vue filename=src/components/ProductCompo.vue

<template>
  <div class="container d-flex justify-content-center mt-2">
    <div class="row gap-2">
      <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 class="btn btn-dark" @click="editPro(item.id)">Edit</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']" />
          <font-awesome-icon :icon="['fas', 'user-secret']" />
        </div>
      </div>
      <div v-if="this.$store.state.products.length == 0">
        <h5>No Products</h5>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    editPro(id) {
      if (this.$route.path != '/admin/pro/edit/' + id) {
        this.$router.push('/admin/pro/edit/' + id)
      }
    },
  },
  mounted() {
    this.$store.dispatch('fetchProducts')
  }
}
</script>

Here you can see that we are showing the products and it has a button to Edit the product which we use to switch to edit product page.

Product UI Component For User

vue filename=src/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>

For user page we have a button to add to cart which we use to add the product to cart. Also we are using star icon for showing average rating of the product.

In both of the components we are calling fetchProducts action to fetch the products from the backend. To implement this action we will write the api call in the apiService.js file. and define the action in the store/index.js file.

Let's update the code in the apiService.js file. We will not add the fetchProducts instead we will write all the fetch calls which are resposible to fetch all the data related with application for initial load. At inital load we will fetch products, categories.

js filename=static/services/apiServices.js

...
async function fetchCategories() {
  return await fetch(`${API_BASE}/get/categories`, {
    method: 'GET',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

async function fetchProducts() {
  return await fetch(`${API_BASE}/get/products`, {
    method: 'GET',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

export {
  ...
  fetchCategories,
  fetchProducts,
};

Now we will implement the calls in the actions of the store.

js filename=static/store/index.js

const store = createStore({
  state: {
    products: [],
    categories: [],
    authenticatedUser: "" // [tl! add]
  },
  getters: {
    // write your code here
  },
  mutations: {
    ...
    setAuthenticatedUser: (state, user) => {
      state.authenticatedUser = user;
    },
  },
  actions: {
    // write your code here
    async fetchProducts({ commit }) { // [tl! add: start]
      try {
        const data = await fetchProducts();
        commit("setProducts", data);
      } catch (error) {
        console.error(error);
      }
    },
    async fetchCategories({ commit }) {
      try {
        const data = await fetchCategories();
        commit("setCategories", data);
      } catch (error) {
        console.error(error);
      }
    }, // [tl! add: end]
  },
});

export default store;

Now we have the CRUD for Category and Products.

Authticated User

I want implement a feature such that when someone try to access frontend without login, it will make call to the backend to check if the user is currently authenticated or not. If the user is authenticated then it will redirect to the dashboard else it will redirect to the login page.

For that we write a api call in the apiService.js file and call it from the action of the store.

Implementing API service

js filename=static/services/apiServices.js

...
async function fetchAuthUser() {
  return await fetch(`${API_BASE}/auth/user`, {
    method: 'GET',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

export {
  ...
  fetchAuthUser,
}

Now implement the calls in the actions of the store.

js filename=static/store/index.js const store = createStore({ state: { ... } ... actions: { ... async fetchAuthUser({ commit }) { try { const data = await fetchAuthUser(); commit("setAuthenticatedUser", data.resource); } catch (error) { console.error(error); } },
} })


In this utility folder we create helpers for our application.

I want to call this `fetchAuthUser` action whenever someone will try to access any of these dashboards. Admin, Manager or User. Since we have already created a basic common layout `DashCompo.vue` so we directly call this method in the `DashCompo.vue` file in mounted option.

js filename=src/components/DashCompo.vue


Now this action will check if the user is authenticated or not and redirect to the login page if the user is not authenticated.

### Backend implementation

python filename=main.py

...

class AuthUser(Resource): def get(self): verify_jwt_in_request() id = get_jwt_identity() current_user = User.query.filter_by(id=id).first() if not current_user: # if the user doesn't exist or password is wrong, reload the page return {'error': 'wrong credentials'}, 404 else: user_data = { 'id': current_user.id, 'role': current_user.role, 'email': current_user.email, 'image': base64.b64encode( current_user.image ).decode('utf-8') if current_user.image else None } return {'msg': 'User login successfully', 'resource': user_data}, 200 ...

api.add_resource(AuthUser, '/auth/user', '/update/profile/')


## Profile Update

We will add profile update feature in the dashboard of each type of users. Since it is common feature for all type fo user so we will add this in the `DashCompo.vue` file.

js filename=src/views/DashCompo.vue


### Backend Implementation

python filename=main.py

... class AuthUser(Resource): ... @jwt_required() def put(self, id): user = User.query.filter_by(id=id).first() if user: # Handle file upload user.image = request.files['image'].read() db.session.commit() user_data = { 'id': user.id, 'role': user.role, 'email': user.email, # Assuming image is stored as a base64-encoded string 'image': base64.b64encode(user.image).decode('utf-8') } return { 'msg': f"User profile updated successfully in the database", 'resource': user_data}, 201 else: return {'msg': "Not found"}, 404
...


This `@jwt_required()` decorator ensures that only authenticated users can access this route.
Now all type of users will be able to update profile picture.

## Filter by Category

Again this is common feature for all type of users. So we will add it into the `DashCompo.vue` file.

### Frontend Implementation

js filename=src/views/DashCompo.vue


One thing you might have noticed that we have created a slot with name edit i.e `<slot name="edit" :categoryId="category.id"></slot>`. This slot is used to render the edit button for each category. Since Admin and Manager should have this option not user.

### Backend Implementation

python filename=main.py

... class SearchCatResource(Resource): #[tl! add:start] def post(self): data = request.getjson() query = data.get('query') # Search for products and categories based on the query products = Product.query.filter(or( Product.name.ilike(f'%{query}%'), Product.rpu.ilike(f'%{query}%'), Product.manufacture.ilike(f'%{query}%'), Product.description.ilike(f'%{query}%'), Product.expiry.ilike(f'%{query}%'), Product.category.has(Category.name.ilike(f'%{query}%')) )).all() category = Category.query.filter_by(name=query).first() products = Product.query.filter_by(category_id=category.id).all() product_list = [] categories = [] for new_product in products: prod_data = { 'id': new_product.id, 'quantity': new_product.quantity, 'name': new_product.name, 'manufacture': new_product.manufacture.strftime("%Y-%m-%d"), 'expiry': new_product.expiry.strftime("%Y-%m-%d"), 'description': new_product.description, 'rpu': new_product.rpu, 'unit': new_product.unit, # Assuming image is stored as a base64-encoded string 'image': base64.b64encode(new_product.image).decode('utf-8') } if new_product.category not in categories: categories.append(new_product.category) product_list.append(prod_data) categories_list = [] for category in categories: cat = { 'id': category.id, 'name': category.name, } categories_list.append(cat) return {"cat": categories_list, 'pro': product_list}, 200 # [tl! add:end] ... api.add_resource(SearchCatResource, '/search/by/catgory') #[tl! add] ...


## Testing it out

Now you can start the backend and frontend and test all the features whatever we have implemented so far.

To start the backend run the following command:

python3 main.py



To start the frontend run the following command:

npm run dev




Let's commit the change using the following command.

git add . git commit -m "database integrated" ``` Continue to start more-features...