A bid is an offer to purchase an asset at a specific price. Prices are mainly determined by how much an individual is willing to pay to own a specific asset. The bidding process is routinely seen in auctions. Recently, the process has been digitized and introduced to various eCommerce sites. For instance, eBay, a multinational e-commerce company based has an auction format product listing which consists of the starting price, listing end date and bids made for that listing. Auctions usually accept bids for a specific amount of time hence the significance of the listing end date.
Strapi is a headless Content Management System(CMS). This means that it handles all the logic needed to transform and store data in the database without having a user interface. Strapi exposes API endpoints that can be used to manipulate data stored in a database. Furthermore, it handles authentication and provides numerous methods to plugin third-party authentication systems like Google and Facebook Auth. Such features shorten project development timelines enabling developers to focus on the User Interfaces instead of the back-end logic and process.
Socket IO is an event-driven JavaScript module that builds on the WebSocket protocol to provide bi-directional event-driven communication. It is highly scalable and can be used on all major platforms. By hooking Socket IO on the Strapi server instance, we’ll be able to make all data management functions real-time which is essential in an online bidding system.
Lastly, we will be using the Vue framework to consume data from the Strapi server instance. Vue supports two-way binding which enables related UI components to be updated in real-time without compromising on performance while rending DOM objects.
We are going to start by setting up our project folder which will consist of the Strapi instance(back-end) and the Vue application(front-end). Both of which require node installation.
npx create-strapi-app@latest backend --quickstart
Strapi v4.2.0 is the latest stable version at the time of writing this article.
The command will create the backbone of our system, install necessary dependencies and initialize an SQLite Database. You can connect your preferred database using the following guide. Once every dependency has been installed, your default browser will open and render the admin registration page for Strapi.
A collection is a data structure that defines how our data will be represented in the database. We will use the content-type builder plugin to create the necessary fields for each collection.
Firstly, build a product collection by following the steps below.
1. Click on Content-type Builder under plugins in the side navigation bar.
2. Click on Create new collection type.
3. Type Product
for the Display name and click Continue.
4. Click the Text field button.
5. Type name
in the Name field.
6. Click on Add another field.
7. Repeat the above steps for the following fields with the corresponding field types.
- auction_end
- Datetime with datetime(ex: 01/01/2022 00:00AM)
type
- price
- Number with big integer
format
- image
- Media (Multiple Media)
- weight
- Number with decimal
format
- bid_price
- Number with big integer
format
- description
- Rich Text
- auction_start
- Datetime with datetime(ex: 01/01/2022 00:00AM)
type
- available
- Boolean
8. Click the Save button and wait for the changes to be applied
We are going to use the auction_end
field to create a countdown by getting the difference between the server time and the value of that field. We’ve selected multiple media on the image
field because we will be using bootstrap’s carousel code block to showcase the different images of the product. Finally, the price
field will be the base price of a product. The bid_price
will be the sum of a bid value and the current bid_price. Initially, both the price
and bid_price
are the same but once bids are created the bid_price
should increase in real-time.
We are going to repeat the above processes but in the account context. Use the steps below to guide you through.
Account
for the Display Name and click Continue.balance
in the Name field and select the big integer
Number format.user
- On the other dialog select **User(from: user-permissions)**
and type account
on the field name input
- Select the option that illustrates two circles joined by one line as shown below
A relation is used to describe two database entities that are related in our context, the account table will be related to the user table. The relation is a one-to-one relationship meaning that each user will have only one account and an account can only be linked to one specific user at a time.
The balance field will be used to save the number of funds needed to purchase a product. A user will only be able to bid on products whose bid_price
is less than their current account balance
. In simpler terms, you can only bid on products you can afford to purchase on the system.
B
id for the Display name and click Continue.value
in the Name field.account
- On the other dialog select **Account**
and type bids
on the field name input
- Select the option that illustrates one circle on the right joined by many circles on the left as shown below
Unlike the User-Account relation we had created, the Bid-Account relation is a one-to-many relationship. This means that an account can have many bids and a bid can only be associated with one account at a time.
Click Relation
account
**Product**
and type bids
on the field name inputThe Bid-Product relation ensures that each bid is associated with a product. This enables us to track the bid_price of a product. The sum of all bid value
and a product’s price
is equal to that product’s bid_price
.
After creating and saving the collection types, Strapi modifies our project directory structure. Each collection type has its folder in the /src/api
folder containing route
, controller
, content-types
and services
directories. We will be modifying the contents in the services
directory for each collection to manipulate the data associated with that specific collection we had created.
Services are reusable functions that manipulate data associated with a specific collection. They can be generated using Strapi’s interactive CLI or manually by creating a JavaScript file in the collection type’s project directory ./src/api/[collection-type-name]/services/
We are going to create two functions that will help us query and add records associated with accounts.
1 ./src/api/account/services/account.js
2
3 'use strict';
4 /**
5 * account service.
6 */
7 const { createCoreService } = require('@strapi/strapi').factories;
8 module.exports = createCoreService('api::account.account', ({ strapi }) => ({
9 newUser(user_id) {
10 return strapi.service('api::account.account').create({
11 data: {
12 balance: 0, user: user_id
13 }
14 });
15 },
16 getUserAccount(user_id) {
17 return strapi.db.query('api::account.account').findOne({
18 where: { user: user_id },
19 })
20 }
21 }));
**newUser**
function enables us to associate a newly registered user to an account. It takes one parameter which is the primary key from the User table. The parameter user_id
is used to create an account for that specific user. Each account will have an initial balance
value of 0.The **getUserAccount**
function enables us to fetch an account associated with a specific user. It also takes user_id
as a parameter. We then use a function from Strapi’s Query Engine API to fetch the record. We are specifically using the findOne function because each account is linked to one user.
We’ll create two functions, one to load bids associated with a specific product and the other one to update the bid price.
1 ./src/api/product/services/product.js
2
3 'use strict';
4 /**
5 * product service.
6 */
7 const { createCoreService } = require('@strapi/strapi').factories;
8 module.exports = createCoreService('api::product.product', ({ strapi }) => ({
9 loadBids(id) {
10 return strapi.entityService.findOne('api::product.product', id, {
11 fields: "*",
12 populate: {
13 bids: {
14 limit: 5,
15 sort: 'createdAt:desc',
16 populate: {
17 account: {
18 fields: ['id'],
19 populate: {
20 user: {
21 fields: ['username']
22 }
23 }
24 }
25 }
26 },
27 image: true
28 },
29 });
30 },
31 async findAndUpdateBidPrice(found, price) {
32 return strapi.entityService.update('api::product.product', found.id, {
33 data: {
34 bid_price: parseInt(found.bid_price) + parseInt(price)
35 },
36 });
37 }
38 }));
**loadBids**
function takes the product id as a parameter which is then used to fetch a specific product. We must explicitly specify which relations we want to load this ensures that we only request attributes that we are sure will be used by our frontend application. We loaded the product image, bids and nested relations under the bid relation so that we can view which user made a specific bid. We also sorted the bids according to the createdAt
field so that the most recent bids will always be displayed first on our Vue application.**findAndUpdateBidPrice**
takes two parameters. The first parameter is the product while the second parameter is the bid price. We are passing a product object as a param because we need to access some fields contained in the object. Before saving the new bid_price, we must ensure that the value provided is an integer. We use the parseInt(arg0)
to accomplish this.Plugins are the backbone of the majority of the features Strapi supports. They can be tweaked and extended to add more functionality or custom logic depending on an application’s objective. By default Strapi ships with the following plugins:
Since the user role and permission plugin is preinstalled on every Strapi application we need to access its source code from the node_modules
folder. The specific folder we are interested in is ./node_modules/@strapi/plugin-users-permissions
and we’ll copy the callback and register functions from ./node_modules/@strapi/plugin-users-permissions/server/controllers/auth.js
. To extend and override those functions from that plugin we need to create strapi-server.js in ./src/extensions/users-permissions
The functions we copied are a lot to consume but basically, the callback function is called every time a user wants to log in. If the credentials are valid the server responds with the JWT token and some basic user information such as the username, email and user_id. In the callback function, just before the user details are sent, we call the getUserAccount function we had created earlier. We append the account balance to the response so that we can access it later on our Vue application.
1 const user = await getService('providers').connect(provider, ctx.query);
2 //Import the account service to fetch account details
3 const account = await strapi.service('api::account.account').getUserAccount(user.id);
4 ctx.send({
5 jwt: getService('jwt').issue({ id: user.id }),
6 user: {
7 ...await sanitizeUser(user, ctx),
8 balance: account.balance, account: account.id },
9 });
The register function is called each time someone wants to create an account on the system. Once the details provided are valid, the server responds with the JWT token and basic user details. Just before the response is sent, we call the newUser function. This ensures that every new user has an account immediately they sign up.
1 if (!settings.email_confirmation) {
2 params.confirmed = true;
3 }
4 const user = await getService('user').add(params);
5 const account = await strapi.service('api::account.account').newUser(user.id);
6 const sanitizedUser = await sanitizeUser(user, ctx);
7 if (settings.email_confirmation) {
8 try {
9 await getService('user').sendConfirmationEmail(sanitizedUser);
10 }
11 catch (err) {
12 throw new ApplicationError(err.message);
13 }
14 return ctx.send({
15 user: {
16 ...sanitizedUser,
17 balance: account.balance,
18 account: account.id
19 } });
20 }
21 const jwt = getService('jwt').issue(_.pick(user, ['id']));
22 return ctx.send({
23 jwt,
24 user: { ...sanitizedUser, balance: account.balance, account: account.id },
25 });
26
27The full `./src/extensions/users-permissions/strapi-server.js` can be found on the [backend repo](https://github.com/i1d9/strapi-bids-backend/blob/master/src/extensions/users-permissions/strapi-server.js).
28
29
30> The register function is called on POST requests made at /api/auth/local/register
31> The callback function is called on POST requests made at /api/auth/local/
32# Socket IO Initialization
33
34To use the Socket IO module, we need to install it first. Run the command below to install it in the project’s `package.json` file.
35
36```bash
37 npm install socket.io
38 # OR
39 yarn add socket.io
The latest version of socket.io was 4.5.1 when I was writing this article
The socket needs to be instantiated before the server starts since Socket IO listens on the same address and port number. Open ./src/index.js
, the file contains functions that run before the Strapi application is started. We are going to add our code within the bootstrap function block. We specify the Cross-Origin Resource Sharing(CORS) object so that the server knows where the request has been made from. The address http://localhost:8080
is the default endpoint for Vue applications in the development environment.
1 bootstrap({ strapi }) {
2
3 let interval;
4 var io = require('socket.io')(strapi.server.httpServer, {
5 cors: {
6 origin: "http://localhost:8080",
7 methods: ["GET", "POST"]
8 }
9 });
10
11 io.on('connection', function (socket) {
12 if (interval) clearInterval(interval);
13 console.log('User connected');
14
15 interval = setInterval(() => io.emit('serverTime', { time: new Date().getTime() }) , 1000);
16
17 //Load a Product's Bids
18 socket.on('loadBids', async (data) => {
19 let params = data;
20 try {
21 let data = await strapi.service('api::product.product').loadBids(params.id);
22 io.emit("loadBids", data);
23 } catch (error) {
24 console.log(error);
25 }
26 });
27
28 socket.on('disconnect', () => {
29 console.log('user disconnected');
30 clearInterval(interval);
31 });
32 });
33
34 //Make the socket global
35 strapi.io = io
36 }
Socket IO is an event-driven module therefore we will be listening for events at specified event names. For instance, the loadBids
will be triggered to respond with bids when a client sends a payload containing the product_id. The response will be generated by the product collection service function we had created earlier.
Within the connection
event block, we have an interval function that sends the server time to the client after every second(1000 ms). The server time will be used to feed the count-down function on the client-side. Once a socket connection is terminated the disconnect
event is triggered and clears the interval.
Since we are using Strapi’s authentication system, we could verify JWT tokens before a connection is made. We will use the users-permission plugin once again to perform the verification. Invalid tokens will be rejected and the connection will not be accepted. We’ll add the authentication login before the connection
event.
1 let interval;
2 var io = require('socket.io')(strapi.server.httpServer, {
3 cors: {
4 origin: "http://localhost:8080",
5 methods: ["GET", "POST"]
6 }
7 });
8
9 io.use(async (socket, next) => {
10 try {
11 //Socket Authentication
12 let result = await strapi.plugins[
13 'users-permissions'
14 ].services.jwt.verify(socket.handshake.query.token);
15 //Save the User ID to the socket connection
16 socket.user = result.id;
17 next();
18 } catch (error) {
19 console.log(error)
20 }
21 }).on('connection', function (socket) {});
We saved the user id to the socket connection so that we can reference which user is creating a bid. Add the block below to handle the new bid logic.
1 socket.on('makeBid', async (data) => {
2 let params = data;
3 try {
4 //Get a specific product
5 let found = await strapi.entityService.findOne('api::product.product', params.product, { fields: "bid_price" });
6
7 //Load the user's account
8 const account = await strapi.service('api::account.account').getUserAccount(socket.user);
9
10 //Check whether user has enough more to make the bid
11 if (parseInt(account.balance) >= parseInt(found.bid_price)) {
12 //Make new Bid
13 await strapi.service('api::bid.bid').makeBid({ ...params, account: account.id });
14 //Update the product's bid price
15 let product = await strapi.service('api::product.product').findAndUpdateBidPrice(found, params.bidValue);
16
17 let updatedProduct = await strapi.service('api::product.product').loadBids(product.id);
18
19 //Send the bids including the new one
20 io.emit("loadBids", updatedProduct);
21 } else {
22 console.log("Balance Is low")
23 }
24
25 } catch (error) {
26 console.log(error);
27 }
28 });
The makeBid
event checks the user has enough funds in their account before a bid is created. We then call the service functions from the account, product and bid collections.
Finally, we need to make the product collection available to authenticated users only. This will ensure that products are loaded when HTTP GET requests are made to the http://localhost:1337/api/products
endpoint.
Bidder
in the name fieldCan create bids and view product listings
in the description field.Select find
and findOne
then click the save button
Click on the Advanced settings button in the user & permission plugin then select Bidder
as the default role for an authenticated user. This ensures that every new user is automatically a bidder and they can view the product listing if their jwt token is appended to a GET request to /api/products
and /api/products/:id
We are going to set up our front-end Vue application generator using the following command.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
Once the CLI has been installed successfully, run the following command to create the project.
vue create frontend
We will be making HTTP requests with Axios HTTP Client. Install it using the following command
npm install axios
# OR
yarn add axios
Once authenticated, we will append our jwt Token to every request sent to the back-end using Axios. If the token is successfully verified, the server will respond with the resource we requested.
We will be sending and receiving data using Socket IO’s front-end library. Install it using the following command.
npm install socket.io-client
# OR
yarn add socket.io-client
We will use our jwt Token to authenticate our socket connections. We will use the same event names used in the server setup to enable us to receive and send data.
This library will be used to map our UI components to routes. Install it using the command below.
npm install vue-router
# OR
yarn add vue-router
We’ll create five components, four of which will be mapped to routes. Feel free to use any CSS framework to style up your components. I’ll be using bootstrap 5 which you could install with the command below.
npm install bootstrap
# OR
yarn add bootstrap
We’ll have the following project structure, grouping related components together.
Each Vue component has its state however we will need some state values to be accessed globally. In our case, we’ll need to access the jwt token from every component to authenticate the socket connections and every request we send to the Strapi back-end. We’ll use vuex which is a state management library for Vue.js applications and vuex-persistedstate, which is a vuex plugin that allows us to store the global state to the browser's local storage instance. To get started create store.js
inside the src
directory.
1 ./src/store.js
2
3 import Vuex from 'vuex';
4 import axios from 'axios';
5 import createPersistedState from "vuex-persistedstate";
6 const dataStore = {
7 state() {
8 return {
9 auth: null//The variable we want to access globally
10 }
11 },
12 mutations: {
13 setUser(state, user) {
14 state.auth = user
15 },
16 logOut(state) {
17 state.auth = null
18 }
19 },
20 getters: {
21 getUser(state) {
22 return state.auth;
23 },
24 isAuthenticated: state => !!state.auth,
25 },
26 actions: {
27 //Register New Users.
28 async Register({ commit }, form) {
29 const json = JSON.stringify(form);
30 const { data } = await axios
31 .post('http://localhost:1337/api/auth/local/register', json, {
32 headers: {
33 'Content-Type': 'application/json'
34 }
35 });
36 //Populate the Auth Object
37 await commit('setUser', { ...data.user, token: data.jwt });
38 },
39 //Authenticate a returning user.
40 async LogIn({ commit }, form) {
41
42 const json = JSON.stringify(form);
43 const { data } = await axios
44 .post('http://localhost:1337/api/auth/local/', json, {
45 headers: {
46 'Content-Type': 'application/json'
47 }
48 });
49 //Populate the Auth Object
50 await commit('setUser', { ...data.user, token: data.jwt });
51 },
52 async LogOut({ commit }) {
53 let user = null
54 commit('logOut', user);
55 }
56 }
57 }
58 export default new Vuex.Store({
59 modules: {
60 dataStore
61 },
62 plugins: [createPersistedState()]
63 })
The dataStore
object contains:
setUser
mutation invoked by the store’s commit functionWe’ll use the vue-router library to render components that match specific routes. We’ll also add some logic before routing to make sure that only authenticated users can access certain UI components. We call the store getter function isAuthenticated
to achieve that.
1****
2 ./src/main.js
3 import { createApp, h } from 'vue'
4 //Bootstrap CSS Framework
5 import "bootstrap/dist/css/bootstrap.min.css"
6 import App from './App.vue'
7 import { createRouter, createWebHashHistory } from "vue-router";
8 //Import the state manager
9 import store from './store';
10 //Product Components
11 import ProductList from "./components/Product/List.vue";
12 import ProductDetail from "./components/Product/Detail.vue";
13 //Authentication Components
14 import LoginPage from "./components/Auth/Login.vue";
15 import RegisterPage from "./components/Auth/Register.vue";
16
17 //Mapping Routes to Components
18 const routes = [
19 { path: "/", component: ProductList, meta: { requiresAuth: true },},
20 { path: "/:id", component: ProductDetail, meta: { requiresAuth: true },},
21 { path: "/login", component: LoginPage, meta: { requiresAuth: false },},
22 { path: "/register", component: RegisterPage, meta: { requiresAuth: false }},
23 ];
24 const router = createRouter({
25 history: createWebHashHistory(),
26 routes,
27 });
28 router.beforeEach((to, from) => {
29 if (to.meta.requiresAuth && !store.getters.isAuthenticated) {
30 return {
31 path: '/login',
32 // save the location we were at to come back later
33 query: { redirect: to.fullPath },
34 }
35 }
36 });
37 const app = createApp({
38 render: () => h(App),
39 });
40 //Add the router to the Application
41 app.use(router);
42 //Add the state manager to the Vue Application.
43 app.use(store);
44 app.mount('#app');
45 //Bootstrap JS Helpers
46 import "bootstrap/dist/js/bootstrap.js"
Finally, to make our mapped components render, we need to modify Vue’s main UI component which is the parent component for all the pages we are going to create. The route-view
tag will be responsible for loading components based on the route. The NavBar
component will be visible across all components.
1 ./src/App.vue
2 <template>
3 <NavBar />
4 <router-view :key="$route.fullPath"></router-view>
5 </template>
6 <script>
7 import NavBar from "./components/Nav.vue";
8 export default {
9 name: 'App',
10 components: {
11 NavBar
12 }
13 }
14 </script>
The Login Page is only rendered when an HTTP GET request is made on http://localhost:8080/login
. We use a simple HTML form to capture the user’s email and password and then pass it to the LogIn
action we had created. The user will be redirected to http://localhost:8080/
if the credentials are correct.
1 ./src/components/Auth/Login.vue
2 <template>
3 <div class="container my-5">
4
5 <form @submit.prevent="submit" class="d-flex flex-column">
6 <div class="mb-3">
7 <input type="email" class="form-control" placeholder="Email Address" name="email" v-model="form.email" />
8 </div>
9
10 <div class="mb-3">
11 <input class="form-control" type="password" name="password" v-model="form.password" placeholder="Password" />
12 </div>
13
14 <button type="submit" class="btn btn-success m-1">Login</button>
15
16 <router-link to="/register" class="btn btn-outline-primary m-1">Register</router-link>
17
18 </form>
19 <p v-if="showError" id="error">Invalid Email/Password</p>
20 </div>
21 </template>
22 <script>
23 import { mapActions } from "vuex";
24 export default {
25 name: " LoginPage",
26 data() {
27 return {
28 form: {
29 email: "",
30 password: "",
31 },
32 showError: false
33 }
34 },
35 methods: {
36 ...mapActions(["LogIn"]),
37 async submit() {
38 try {
39 await this.LogIn({
40 identifier: this.form.email,
41 password: this.form.password
42 });
43 this.$router.push(this.$route.query.redirect)
44 this.showError = false
45 } catch (error) {
46 this.showError = true
47 }
48 }
49 }
50 }
51 </script>
We added some bootstrap CSS styles to make the page mobile responsive. The Register Button directs the user to the registration page at http://localhost:8080/register
The Register page is used to create new accounts. By default, all users are [Bidders](https://www.dropbox.com/scl/fi/jrha49e6tg0hjx1xx4csp/How-to-create-a-real-time-Bidding-App-using-Strapi-v4-Vue-and-Socket-IO.paper?dl=0&rlkey=di2crkfkq9ckjsafeadke6obw#:uid=540338709510983306879551&h2=Creating-new-user-roles)
and they all have a monetary account. If the server successfully creates the authentication account the user is automatically assigned a monetary account.
1 ./src/components/Auth/Register.vue
2 <template>
3 <div class="container my-5">
4 <form @submit.prevent="submit" class="d-flex flex-column">
5 <div class="mb-3">
6 <input type="text" class="form-control" placeholder="Username" name="username" v-model="form.username" />
7 </div>
8
9 <div class="mb-3">
10 <input type="email" class="form-control" placeholder="Email Address" name="email"
11 v-model="form.email" />
12 </div>
13
14 <div class="mb-3">
15 <input type="password" class="form-control" name="password" v-model="form.password" placeholder="Password" />
16 </div>
17
18 <button type="submit" class="btn btn-success m-1">Create Account</button>
19 <router-link to="/login" class="btn btn-outline-primary m-1">Login</router-link>
20
21 </form>
22 <p v-if="showError" id="error">
23 Could not create an account with the details provided
24 </p>
25
26 </div>
27 </template>
28 <script>
29 import { mapActions } from "vuex";
30 export default {
31 name: " RegisterPage",
32 data() {
33 return {
34 form: {
35 username: "",
36 email: "",
37 password: "",
38 },
39 showError: false
40 }
41 },
42 methods: {
43 ...mapActions(["Register"]),
44 async submit() {
45 try {
46 await this.Register({
47 username: this.form.username,
48 password: this.form.password,
49 email: this.form.email,
50 });
51 this.$router.push("/");
52 this.showError = false
53 } catch (error) {
54 this.showError = true
55 }
56 }
57 }
58 }
59 </script>
This component formats the product details within a Bootstrap-styled. The details are passed into the components via props. Furthermore the component displays the time left till the auction ends(auction_end
field that we defined in the product collection type). Once the component has been created we initiated an interval function that computes the countdown values based on the serverTime
prop whose value is fetched in real time from the socket io instance. This component will be rendered for all available products in the Listing component.
1 ./src/components/Product/Card.vue
2 <template>
3 <div class="card m-3 p-3 position-relative">
4 <img :src="'http://localhost:1337' + this.product.image.data[0].attributes.formats.medium.url" class="card-img-top" alt={{content.name}} />
5
6 <div class="card-body">
7 <router-link :to="'/' + this.id">{{ content.name }}</router-link>
8 <p>KES {{ content.price }}</p>
9 <h6 class="card-subtitle mb-2 text-muted">{{ countDownValue }} </h6>
10 </div>
11 <div class="position-absolute top-0 end-0 p-1 m-2 btn-outline-danger">
12 Live
13 </div>
14 </div>
15 </template>
16 <script>
17 export default {
18 name: "CardComponent",
19 props: ['product', 'serverTime', 'id'],
20 data() {
21 return {
22 content: this.product,
23 countDownInterval: null,
24 countDownValue: ''
25 }
26 },
27 methods: {
28 countDown() {
29 // Get today's date and time
30 var now = this.serverTime;
31 // Find the distance between now and the count down date
32 var distance = new Date(this.product.auction_end) - now;
33 // Time calculations for days, hours, minutes and seconds
34 var days = Math.floor(distance / (1000 * 60 * 60 * 24));
35 var hours = Math.floor((distance%(1000 *60 * 60 * 24))/(1000 * 60 * 60));
36 var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
37 var seconds = Math.floor((distance % (1000 * 60)) / 1000);
38 this.countDownValue=`${days} Days ${hours} hrs ${minutes} minutes ${seconds} seconds`;
39 }
40 },
41 created() {
42 this.countDownInterval = setInterval(() => this.countDown() , 1000);
43 }
44 }
45 </script>
This component fetches the products from the Strapi server via a normal HTTP GET request appended with a jwt Token in the header. We specified the client’s address(http://localhost:8080
) on the server to prevent CORS errors. Once the products have been loaded we use the Socket IO instance to get the server time which will be used to compute the countdown of each product. The server time and product details will be passed as props to the card component.
1 ./src/components/Product/List.vue
2 <template>
3 <div class="container p-2">
4 <div class="row">
5 <div class="col-lg-3 col-md-4" v-for="product in products" :key="product.id">
6 <Card :product="product.attributes" :serverTime="serverTime" :id="product.id" />
7 </div>
8 </div>
9 </div>
10 </template>
11 <script>
12 import axios from 'axios';
13 import socketIOClient from "socket.io-client";
14 import Card from './Card.vue';
15 export default {
16 name: "ProductList",
17 data() {
18 return {
19 products: [],
20 socket: socketIOClient("http://localhost:1337", {
21 query: {
22 token: this.$store.getters.getUser.token
23 }
24 }),
25 serverTime: null,
26 };
27 },
28 methods: {
29 async getproducts() {
30 try {
31 const response = await axios.get("http://localhost:1337/api/products?populate=image&&name",{
32 headers: {
33 'Authorization': `Bearer ${this.$store.getters.getUser.token}`
34 }
35 });
36 this.products = response.data.data;
37 }
38 catch (error) {
39 console.log(error);
40 }
41 }
42 },
43 created() {
44 this.getproducts();
45 this.socket.on("serverTime", (data) => {
46 this.serverTime = data.time
47 });
48 },
49 components: { Card }
50 }
51 </script>
Feel free to add products from the admin dashboard. Play around with the
auction_end
field to see how the countdown changes.
This component loads the bids through an authenticated socket connection. It also captures a user’s bid value and sends it to the server. The bid is then broadcasted to all clients viewing the same product. We also use the server time to compute the countdown time.
1 <template>
2 <div class="container mt-4 m-md-3 m-lg-3 ">
3 <div class="row">
4 <div class="col-md-7 col-lg-7">
5 <div id="carouselExampleSlidesOnly" class="carousel slide" data-bs-ride="carousel">
6 <div class="carousel-inner">
7 <div v-for="image in this.product.image" :key="image.id">
8 <img :src="'http://localhost:1337' + (image.formats.medium.url)" class="card-img-top" alt={{content.name}} /></div>
9 <div class="carousel-item">
10 <img src="" class="d-block w-100" alt=""></div>
11 <div class="carousel-item"><img src="" class="d-block w-100" alt="">
12 </div></div></div>
13 <p>{{ this.product.description }}</p></div>
14 <div class="col-md-5 col-lg-5"> Time left: {{ countDown(this.serverTime) }}
15 <div class="card m-2 p-3">
16 <p>Current Price: KES {{ this.product.bid_price }} </p>
17 <div class="overflow-auto" style="height: 10rem;">
18 <div v-if="this.bids.length > 0">
19 <div v-for="bid in bids" :key="bid.id">
20 <div class="border p-3 m-2">
21 <p>{{ bid.account.user.username }}</p>
22 <p>KES {{ bid.value }}</p>
23 </div>
24 </div>
25 </div>
26 <div class="card text-center m-2 p-3" v-else>
27 <span>No Bids available</span>
28 </div>
29 </div>
30 </div>
31 <div class="m-2 d-flex flex-column">
32 <input type="number" v-model="bidValue" placeholder="Bid Value" class="form-control" min="1" />
33 <button type="button" @click="makeBid" class="btn btn-outline-warning">Bid</button>
34 </div>
35 </div>
36 </div>
37 </div>
38 </template>
39 <script>
40 import socketIOClient from "socket.io-client";
41 export default {
42 name: "ProductDetail",
43 data() {
44 return {
45 bidValue: 1,
46 product: {
47 },
48 bids: [],
49 socket: socketIOClient("http://localhost:1337", {
50 query: {
51 token: this.$store.getters.getUser.token
52 }
53 }),
54 serverTime: new Date(),
55 countDownInterval: null,
56 countDownValue: ''
57 }
58 },
59 methods: {
60 countDown(now) {
61 // Find the distance between now and the count down date
62 var distance = new Date(this.product.auction_end) - now;
63 // Time calculations for days, hours, minutes and seconds
64 var days = Math.floor(distance / (1000 * 60 * 60 * 24));
65 var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
66 var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
67 var seconds = Math.floor((distance % (1000 * 60)) / 1000);
68 return `${days} Days ${hours} hrs ${minutes} minutes ${seconds} seconds`
69 },
70 makeBid() {
71 if (this.bidValue > 0) {
72 this.socket.emit("makeBid", { bidValue: this.bidValue, user: this.$store.getters.getUser.id, product: this.product.id });
73 }
74 }
75 },
76 created() {
77 this.socket.emit("loadBids", { id: this.$route.params.id });
78 this.socket.on("loadBids", (data) => {
79 this.product = data;
80 this.bids = data.bids;
81 });
82 },
83 mounted() {
84 this.socket.on("serverTime", (data) => this.serverTime = data.time);
85 }
86 }
87 </script>
We are looping through all available product images so that they can be displayed on bootstrap’s carousel. The Description of the Product is displayed beneath the carousel items while all the bids and the time left are displayed on the right side of the image. The UI can be tweaked to your liking. Each bid displays the username of the person who made the bid and the bid’s value. The sum of all bids is added to the base price to get the bid_price
This article guided you on how to build a real-time bidding system with Vue.js as the frontend and Strapi as the backend. We created a full-duplex communication channel with the help of the socket io client and server library to manipulate and broadcast data to specific event listeners. The article also showcased how to extend Strapi plugins and build custom collection services.
You can download the source code from the following repositories: