Build an e-commerce application using Laravel and Vue Part 3: Create the frontend to consume APIs

ecommerce-laravel-vue-part-3-header.png

This three-part tutorial series shows how to build an e-commerce application with Laravel and Vue. It includes authentication using Passport, and a simple sqlite database. In part three, complete the frontend with Vue, and see how to use the APIs created in the previous sections.

Introduction

In this chapter, we will finish the frontend of the Laravel application and consume the APIs we already made.

In the previous chapters (part 1, part 2), we created the backend for our application, created some API endpoints and set up Vue which would enable us create the core of the frontend.

When we are done, our application will look like this:

laravel ecommerce app preview

Prerequisites

To follow along in this part, you must have completed the first and second parts of the series and you need to have all the requirements from them.

Making the authentication pages

In the previous part, we created all the view files for our Vue application, although we did not add content to all of them. Let’s start with the login and register pages.

Open the resources/assets/js/views/Login.vue file and paste in the following code:

1<template>
2        <div class="container">
3            <div class="row justify-content-center">
4                <div class="col-md-8">
5                    <div class="card card-default">
6                        <div class="card-header">Login</div>
7                        <div class="card-body">
8                            <form>
9                                <div class="form-group row">
10                                    <label for="email" class="col-sm-4 col-form-label text-md-right">E-Mail Address</label>
11                                    <div class="col-md-6">
12                                        <input id="email" type="email" class="form-control" v-model="email" required autofocus>
13                                    </div>
14                                </div>
15                                <div class="form-group row">
16                                    <label for="password" class="col-md-4 col-form-label text-md-right">Password</label>
17                                    <div class="col-md-6">
18                                        <input id="password" type="password" class="form-control" v-model="password" required>
19                                    </div>
20                                </div>
21                                <div class="form-group row mb-0">
22                                    <div class="col-md-8 offset-md-4">
23                                        <button type="submit" class="btn btn-primary" @click="handleSubmit">
24                                            Login
25                                        </button>
26                                    </div>
27                                </div>
28                            </form>
29                        </div>
30                    </div>
31                </div>
32            </div>
33        </div>
34    </template>

Above we have a login form. We don’t have much functionality yet so let’s append the following code to the same file to add some Vue scripting:

1<script>
2        export default {
3            data() {
4                return {
5                    email: "",
6                    password: ""
7                }
8            },
9            methods: {
10                handleSubmit(e) {
11                    e.preventDefault()
12                    if (this.password.length > 0) {
13                        let email = this.email
14                        let password = this.password
15
16                        axios.post('api/login', {email, password}).then(response => {
17                            let user = response.data.user
18                            let is_admin = user.is_admin
19
20                            localStorage.setItem('bigStore.user', JSON.stringify(user))
21                            localStorage.setItem('bigStore.jwt', response.data.token)
22
23                            if (localStorage.getItem('bigStore.jwt') != null) {
24                                this.$emit('loggedIn')
25                                if (this.$route.params.nextUrl != null) {
26                                    this.$router.push(this.$route.params.nextUrl)
27                                } else {
28                                    this.$router.push((is_admin == 1 ? 'admin' : 'dashboard'))
29                                }
30                            }
31                        });
32                    }
33                }
34            }
35        }
36    </script>

Above we have a handleSubmit method that is fired when the form is submitted. In this method we attempt to authenticate using the API. If the authentication is successful, we save the access token and user data in localStorage so we can access them across our app.

We also emit a loggedIn event so the parent component can update as well. Lastly, we check if the user was sent to the login page from another page, then send the user to that page. If the user came to login directly, we check the user type and redirect the user appropriately.

Next, open the resources/assets/js/views/Register.vue file and paste in the following:

1<template>
2        <div class="container">
3            <div class="row justify-content-center">
4                <div class="col-md-8">
5                    <div class="card card-default">
6                        <div class="card-header">Register</div>
7                        <div class="card-body">
8                            <form>
9                                <div class="form-group row">
10                                    <label for="name" class="col-md-4 col-form-label text-md-right">Name</label>
11                                    <div class="col-md-6">
12                                        <input id="name" type="text" class="form-control" v-model="name" required autofocus>
13                                    </div>
14                                </div>
15                                <div class="form-group row">
16                                    <label for="email" class="col-md-4 col-form-label text-md-right">E-Mail Address</label>
17                                    <div class="col-md-6">
18                                        <input id="email" type="email" class="form-control" v-model="email" required>
19                                    </div>
20                                </div>
21                                <div class="form-group row">
22                                    <label for="password" class="col-md-4 col-form-label text-md-right">Password</label>
23                                    <div class="col-md-6">
24                                        <input id="password" type="password" class="form-control" v-model="password" required>
25                                    </div>
26                                </div>
27                                <div class="form-group row">
28                                    <label for="password-confirm" class="col-md-4 col-form-label text-md-right">Confirm Password</label>
29                                    <div class="col-md-6">
30                                        <input id="password-confirm" type="password" class="form-control" v-model="password_confirmation" required>
31                                    </div>
32                                </div>
33                                <div class="form-group row mb-0">
34                                    <div class="col-md-6 offset-md-4">
35                                        <button type="submit" class="btn btn-primary" @click="handleSubmit">
36                                            Register
37                                        </button>
38                                    </div>
39                                </div>
40                            </form>
41                        </div>
42                    </div>
43                </div>
44            </div>
45        </div>
46    </template>

Above we have the HTML for the registration form. Let’s add the script for the component below the closing template tag:

1<script>
2    export default {
3        props : ['nextUrl'],
4        data(){
5            return {
6                name : "",
7                email : "",
8                password : "",
9                password_confirmation : ""
10            }
11        },
12        methods : {
13            handleSubmit(e) {
14                e.preventDefault()
15                if (this.password !== this.password_confirmation || this.password.length <= 0) {
16                    this.password = ""
17                    this.password_confirmation = ""
18                    return alert('Passwords do not match')
19                }
20                let name = this.name
21                let email = this.email
22                let password = this.password
23                let c_password = this.password_confirmation
24                axios.post('api/register', {name, email, password, c_password}).then(response => {
25                    let data = response.data
26                    localStorage.setItem('bigStore.user', JSON.stringify(data.user))
27                    localStorage.setItem('bigStore.jwt', data.token)
28                    if (localStorage.getItem('bigStore.jwt') != null) {
29                        this.$emit('loggedIn')
30                        let nextUrl = this.$route.params.nextUrl
31                        this.$router.push((nextUrl != null ? nextUrl : '/'))
32                    }
33                })
34            }
35        }
36    }
37    </script>

The register component operates similarly to the login component. We send the user data to the API for authentication and if we get a favourable response we save the token and user to localStorage.

Making the marketplace pages

We had already defined the homepage in the last chapter and returned a list of available products. Now, we are going to make all the other store pages.

Open the resources/assets/js/views/SingleProduct.vue file and paste in the following code:

1<template>
2        <div class="container">
3            <div class="row">
4                <div class="col-md-8 offset-md-2">
5                    <img :src="product.image" :alt="product.name">
6                    <h3 class="title" v-html="product.name"></h3>
7                    <p class="text-muted">{{product.description}}</p>
8                    <h4>
9                        <span class="small-text text-muted float-left">$ {{product.price}}</span>
10                        <span class="small-text float-right">Available Quantity: {{product.units}}</span>
11                    </h4>
12                    <br>
13                    <hr>
14                    <router-link :to="{ path: '/checkout?pid='+product.id }" class="col-md-4 btn btn-sm btn-primary float-right">Buy Now</router-link>
15                </div>
16            </div>
17        </div>
18    </template>
19
20    <script>
21    export default {
22        data(){
23            return {
24                product : []
25            }
26        },
27        beforeMount(){
28            let url = `/api/products/${this.$route.params.id}`
29            axios.get(url).then(response => this.product = response.data)      
30        }
31    }
32    </script>
33
34    <style scoped>
35    .small-text { font-size: 18px; }
36    .title { font-size: 36px; }
37    </style>

Above we have the product as a data attribute, which we use to display information on the page like we did in the Home.vue file.

In the components script we defined Vue’s beforeMount method and fetched the product information there. beforeMount is called before the component is rendered, so it fetches the necessary data for rendering the component. If we get the data after the component has mounted, we would have errors before the component updates.

Next, open the resources/assets/js/views/Checkout.vue file and paste the following code for the HTML template and style:

1<template>
2        <div class="container">
3            <div class="row">
4                <div class="col-md-8 offset-md-2">
5                    <div class="order-box">
6                        <img :src="product.image" :alt="product.name">
7                        <h2 class="title" v-html="product.name"></h2>
8                        <p class="small-text text-muted float-left">$ {{product.price}}</p>
9                        <p class="small-text text-muted float-right">Available Units: {{product.units}}</p>
10                        <br>
11                        <hr>
12                        <label class="row"><span class="col-md-2 float-left">Quantity: </span><input type="number" name="units" min="1" :max="product.units" class="col-md-2 float-left" v-model="quantity" @change="checkUnits"></label>
13                    </div>
14                    <br>
15                    <div>
16                        <div v-if="!isLoggedIn">
17                            <h2>You need to login to continue</h2>
18                            <button class="col-md-4 btn btn-primary float-left" @click="login">Login</button>
19                            <button class="col-md-4 btn btn-danger float-right" @click="register">Create an account</button>
20                        </div>
21                        <div v-if="isLoggedIn">
22                            <div class="row">
23                                <label for="address" class="col-md-3 col-form-label">Delivery Address</label>
24                                <div class="col-md-9">
25                                    <input id="address" type="text" class="form-control" v-model="address" required>
26                                </div>
27                            </div>
28                            <br>
29                            <button class="col-md-4 btn btn-sm btn-success float-right" v-if="isLoggedIn" @click="placeOrder">Continue</button>
30                        </div>
31                    </div>
32                </div>
33            </div>
34        </div>
35    </template>
36
37    <style scoped>
38    .small-text { font-size: 18px; }
39    .order-box { border: 1px solid #cccccc; padding: 10px 15px; }
40    .title { font-size: 36px; }
41    </style>

Below it paste the following for the script:

1<script>
2    export default {
3        props : ['pid'],
4        data(){
5            return {
6                address : "",
7                quantity : 1,
8                isLoggedIn : null,
9                product : []
10            }
11        },
12        mounted() {
13            this.isLoggedIn = localStorage.getItem('bigStore.jwt') != null
14        },
15        beforeMount() {
16            axios.get(`/api/products/${this.pid}`).then(response => this.product = response.data)
17
18            if (localStorage.getItem('bigStore.jwt') != null) {
19                this.user = JSON.parse(localStorage.getItem('bigStore.user'))
20                axios.defaults.headers.common['Content-Type'] = 'application/json'
21                axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('bigStore.jwt')
22            }
23        },
24        methods : {
25            login() {
26                this.$router.push({name: 'login', params: {nextUrl: this.$route.fullPath}})
27            },
28            register() {
29                this.$router.push({name: 'register', params: {nextUrl: this.$route.fullPath}})
30            },
31            placeOrder(e) {
32                e.preventDefault()
33
34                let address = this.address
35                let product_id = this.product.id
36                let quantity = this.quantity
37
38                axios.post('api/orders/', {address, quantity, product_id})
39                     .then(response => this.$router.push('/confirmation'))
40            },
41            checkUnits(e){
42                if (this.quantity > this.product.units) {
43                    this.quantity = this.product.units
44                }
45            }
46        }
47    }
48    </script>

Above we defined the beforeMount method where we fetch product information. Then we have the mounted method where we check authentication status. In the methods property, we defined the checkUnits method that checks how many units the user wants to order, and then we define the placeOrder method that places the order.

Next, open the resources/assets/js/views/Confirmation.vue file and paste the following code:

1<template>
2        <div>
3            <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto">
4                <h2>
5                    <span class="title"><strong>Thank You!</strong></span><br>
6                    <span class="medium-text">Your order has been placed.</span><br>
7                    <router-link :to="{name: 'userboard'}" class="small-link">
8                        See your orders
9                    </router-link>
10                </h2>
11            </div>
12        </div>
13    </template>
14
15    <script>
16    export default {}
17    </script>
18
19    <style scoped>
20    .medium-text { font-size: 36px; }
21    .small-link { font-size: 24px; text-decoration: underline; color: #777; }
22    .product-box { border: 1px solid #cccccc; padding: 10px 15px; }
23    .hero-section { height: 80vh; align-items: center; margin-top: -20px; margin-bottom: 20px; }
24    .title { font-size: 60px; }
25    </style>

This component displays a simple thank you message and provides a link for the users to navigate to their dashboard to see the orders they have placed and the status of these orders. Let’s create the user dashboard next.

Creating the user dashboard

The user dashboard is where the users can see all their orders. Open the resources/assets/js/views/UserBoard.vue file and paste in the following code for the template and style:

1<template>
2        <div>
3            <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto">
4                <h2 class="title">All your orders</h2>
5            </div>
6            <div class="container">
7                <div class="row">
8                    <div class="col-md-12">
9                        <br>
10                        <div class="row">
11                            <div class="col-md-4 product-box" v-for="(order,index) in orders" @key="index">
12                                <img :src="order.product.image" :alt="order.product.name">
13                                <h5><span v-html="order.product.name"></span><br>
14                                    <span class="small-text text-muted">$ {{order.product.price}}</span>
15                                </h5>
16                                <hr>
17                                <span class="small-text text-muted">Quantity: {{order.quantity}}
18                                    <span class="float-right">{{order.is_delivered == 1? "shipped!" : "not shipped"}}</span>
19                                </span>
20                                <br><br>
21                                <p><strong>Delivery address:</strong> <br>{{order.address}}</p>
22                            </div>
23                        </div>
24                    </div>
25                </div>
26            </div>
27        </div>
28    </template>
29
30    <style scoped>
31    .small-text { font-size: 14px; }
32    .product-box { border: 1px solid #cccccc; padding: 10px 15px; }
33    .hero-section { background: #ababab; height: 20vh; align-items: center; margin-bottom: 20px; margin-top: -20px; }
34    .title { font-size: 60px; color: #ffffff; }
35    </style>

Then for the script paste in the following below the closing style tag:

1<script>
2    export default {
3        data() {
4            return {
5                user : null,
6                orders : []
7            }
8        },
9        beforeMount() {
10            this.user = JSON.parse(localStorage.getItem('bigStore.user'))
11
12            axios.defaults.headers.common['Content-Type'] = 'application/json'
13            axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('bigStore.jwt')
14
15            axios.get(`api/users/${this.user.id}/orders`)
16                 .then(response => this.orders = response.data)
17        }
18    }
19    </script>

In the code above, we fetch all the user’s orders before the component is mounted, then loop through them and display them on the page.

Creating the admin dashboard

The admin dashboard is where new products are added, existing products modified and orders are set as delivered.

For the admin board, we will use four different components, which we will mount based on the url a user is accessing. Let’s see that in action. Open the resources/assets/js/views/Admin.vue file and paste in the following:

1<template>
2        <div>
3            <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto">
4                <h2 class="title">Admin Dashboard</h2>
5            </div>
6            <div class="container">
7                <div class="row">
8                    <div class="col-md-3">
9                        <ul style="list-style-type:none">
10                            <li class="active"><button class="btn" @click="setComponent('main')">Dashboard</button></li>
11                            <li><button class="btn" @click="setComponent('orders')">Orders</button></li>
12                            <li><button class="btn" @click="setComponent('products')">Products</button></li>
13                            <li><button class="btn" @click="setComponent('users')">Users</button></li>
14                        </ul>
15                    </div>
16                    <div class="col-md-9">
17                        <component :is="activeComponent"></component>
18                    </div>
19                </div>
20            </div>
21        </div>
22    </template>
23
24    <script>
25    import Main from '../components/admin/Main'
26    import Users from '../components/admin/Users'
27    import Products from '../components/admin/Products'
28    import Orders from '../components/admin/Orders'
29
30    export default {
31        data() {
32            return {
33                user: null,
34                activeComponent: null
35            }
36        },
37        components: {
38            Main, Users, Products, Orders
39        },
40        beforeMount() {
41            this.setComponent(this.$route.params.page)
42            this.user = JSON.parse(localStorage.getItem('bigStore.  d fuser'))
43            axios.defaults.headers.common['Content-Type'] = 'application/json'
44            axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('bigStore.jwt')
45        },
46        methods: {
47            setComponent(value) {
48                switch(value) {
49                    case "users":
50                        this.activeComponent = Users
51                        this.$router.push({name: 'admin-pages', params: {page: 'users'}})
52                        break;
53                    case "orders":
54                        this.activeComponent = Orders
55                        this.$router.push({name: 'admin-pages', params: {page: 'orders'}})
56                        break;
57                    case "products":
58                        this.activeComponent = Products
59                        this.$router.push({name: 'admin-pages', params: {page: 'products'}})
60                        break;
61                    default:
62                        this.activeComponent = Main
63                        this.$router.push({name: 'admin'})
64                        break;
65                }
66            }
67        }
68    }
69    </script>
70
71    <style scoped>
72    .hero-section { height: 20vh; background: #ababab; align-items: center; margin-bottom: 20px; margin-top: -20px; }
73    .title { font-size: 60px; color: #ffffff; }
74    </style>

In the code above, we import and register four components, which we have not yet created. They’ll be used as components inside Admin.vue parent component.

In our template, we defined the navigation for switching between the components. Each navigation link calls the setComponent method and then passes a value to it. The setComponent method just sets the component using a switch statement.

Let’s create the first component for the Admin component. Create the Main.vue file in resources/assets/js/components/admin directory and paste the following into the file:

1<template>
2        <div class="row">
3            <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text">
4                <a href='/admin/orders'>Orders ({{orders.length}})</a>
5            </div>
6            <hr>
7            <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text">
8                <a href='/admin/products'>Products ({{products.length}})</a>
9            </div>
10            <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text">
11                <a href='/admin/users'>Users ({{users.length}})</a>
12            </div>
13        </div>
14    </template>
15
16    <script>
17    export default {
18        data() {
19            return {
20                user : null,
21                orders : [],
22                products : [],
23                users : []
24            }
25        },
26        mounted() {
27            axios.get('/api/users/').then(response => this.users = response.data)
28            axios.get('/api/products/').then(response => this.products = response.data)
29            axios.get('/api/orders/').then(response => this.orders = response.data)
30        }
31    }
32    </script>
33
34    <style scoped>
35    .big-text { font-size: 28px; }
36    .product-box { border: 1px solid #cccccc; padding: 10px 15px; height: 20vh }
37    </style>

In the code above we are calling the APIs for users, orders and products, and returning their data.

Create the Orders.vue file in resources/assets/js/components/admin and paste the following into the file:

1<template>
2        <div>
3            <table class="table table-responsive table-striped">
4                <thead>
5                    <tr>
6                        <td></td>
7                        <td>Product</td>
8                        <td>Quantity</td>
9                        <td>Cost</td>
10                        <td>Delivery Address</td>
11                        <td>is Delivered?</td>
12                        <td>Action</td>
13                    </tr>
14                </thead>
15                <tbody>
16                    <tr v-for="(order,index) in orders" @key="index">
17                        <td>{{index+1}}</td>
18                        <td v-html="order.product.name"></td>
19                        <td>{{order.quantity}}</td>
20                        <td>{{order.quantity * order.product.price}}</td>
21                        <td>{{order.address}}</td>
22                        <td>{{order.is_delivered == 1? "Yes" : "No"}}</td>
23                        <td v-if="order.is_delivered == 0"><button class="btn btn-success" @click="deliver(index)">Deliver</button></td>
24                    </tr>
25                </tbody>
26            </table>
27        </div>
28    </template>
29
30    <script>
31    export default {
32        data() {
33            return {
34                orders : []
35            }
36        },
37        beforeMount(){
38            axios.get('/api/orders/').then(response => this.orders = response.data)
39        },
40        methods: {
41            deliver(index) {
42                let order = this.orders[index]
43                axios.patch(`/api/orders/${order.id}/deliver`).then(response => {
44                    this.orders[index].is_delivered = 1
45                    this.$forceUpdate()
46                })
47            }
48        }
49    }
50    </script>

In beforeMount we fetch all the orders placed before the component is rendered.

When the Deliver button is clicked, the deliver method is fired. We call the API for delivering orders. To get the change to reflect on the page instantly, we call [$forceUpdate](https://vuejs.org/v2/api/#vm-forceUpdate).

Create the Users.vue file in resources/assets/js/components/admin and paste in the following code:

1<template>
2        <div>
3            <table class="table table-responsive table-striped">
4                <thead>
5                    <tr>
6                        <td></td>
7                        <td>Name</td>
8                        <td>Email</td>
9                        <td>Joined</td>
10                        <td>Total Orders</td>
11                    </tr>
12                </thead>
13                <tbody>
14                    <tr v-for="(user,index) in users" @key="index">
15                        <td>{{index+1}}</td>
16                        <td>{{user.name}}</td>
17                        <td>{{user.email}}</td>
18                        <td>{{user.created_at}}</td>
19                        <td>{{user.orders.length}}</td>
20                    </tr>
21                </tbody>
22            </table>
23        </div>
24    </template>
25
26    <script>
27    export default {
28        data() {
29            return {
30                users : []
31            }
32        },
33        beforeMount() {
34            axios.get('/api/users/').then(response => this.users = response.data)
35        }
36    }
37    </script>

Above we fetch all the user data and then display it on the page.

Next, create the Products.vue file in resources/assets/js/components/admin and paste the following template code:

1<template>
2        <div>
3            <table class="table table-responsive table-striped">
4                <thead>
5                    <tr>
6                        <td></td>
7                        <td>Product</td>
8                        <td>Units</td>
9                        <td>Price</td>
10                        <td>Description</td>
11                    </tr>
12                </thead>
13                <tbody>
14                    <tr v-for="(product,index) in products" @key="index" @dblclick="editingItem = product">
15                        <td>{{index+1}}</td>
16                        <td v-html="product.name"></td>
17                        <td v-model="product.units">{{product.units}}</td>
18                        <td v-model="product.price">{{product.price}}</td>
19                        <td v-model="product.price">{{product.description}}</td>
20                    </tr>
21                </tbody>
22            </table>
23            <modal @close="endEditing" :product="editingItem" v-show="editingItem != null"></modal>
24            <modal @close="addProduct"  :product="addingProduct" v-show="addingProduct != null"></modal>
25            <br>
26            <button class="btn btn-primary" @click="newProduct">Add New Product</button>
27        </div>
28    </template>

Below that paste in the following script code:

1<script>
2    import Modal from './ProductModal'
3
4    export default {
5        data() {
6            return {
7                products: [],
8                editingItem: null,
9                addingProduct: null
10            }
11        },
12        components: {Modal},
13        beforeMount() {
14            axios.get('/api/products/').then(response => this.products = response.data)
15        },
16        methods: {
17            newProduct() {
18                this.addingProduct = {
19                    name: null,
20                    units: null,
21                    price: null,
22                    image: null,
23                    description: null,
24                }
25            },
26            endEditing(product) {
27                this.editingItem = null
28
29                let index = this.products.indexOf(product)
30                let name = product.name
31                let units = product.units
32                let price = product.price
33                let description = product.description
34
35                axios.put(`/api/products/${product.id}`, {name, units, price, description})
36                     .then(response => this.products[index] = product)
37            },
38            addProduct(product) {
39                this.addingProduct = null
40
41                let name = product.name
42                let units = product.units
43                let price = product.price
44                let description = product.description
45                let image = product.image 
46
47                axios.post("/api/products/", {name, units, price, description, image})
48                     .then(response => this.products.push(product))
49            }
50        }
51    }
52    </script>

In the methods property we defined the following methods:

  • newProduct() – called when we want to initiate a new local product object.
  • endEditing() – called when we are done editing a product.
  • addProduct() – called when we are want to add a new product.

We imported a ProductModal component, which we will create next. The modal will be used to edit an existing or create a new product. Double clicking on a product listed fires up the modal for editing the product.

Create the ProductModal.vue file in resources/assets/js/components/admin and paste in the following template and style code:

1<template>
2        <div class="modal-mask">
3            <div class="modal-wrapper">
4                <div class="modal-container">
5                    <div class="modal-header">
6                        <slot name="header" v-html="data.name"></slot>
7                    </div>
8                    <div class="modal-body">
9                        <slot name="body">
10                            Name: <input type="text" v-model="data.name">
11                            Units: <input type="text" v-model="data.units">
12                            Price: <input type="text" v-model="data.price">
13                            <textarea v-model="data.description" placeholder="description"></textarea>
14                            <span >
15                                <img :src="data.image" v-show="data.image != null">
16                                <input type="file" id="file" @change="attachFile">
17                            </span>
18                        </slot>
19                    </div>
20                    <div class="modal-footer">
21                        <slot name="footer">
22                            <button class="modal-default-button" @click="uploadFile">
23                                Finish
24                            </button>
25                        </slot>
26                    </div>
27                </div>
28            </div>
29        </div>
30    </template>
31
32    <style scoped>
33    .modal-mask {
34        position: fixed;
35        z-index: 9998;
36        top: 0;
37        left: 0;
38        width: 100%;
39        height: 100%;
40        background-color: rgba(0, 0, 0, .5);
41        display: table;
42        transition: opacity .3s ease;
43    }
44    .modal-wrapper {
45        display: table-cell;
46        vertical-align: middle;
47    }
48    .modal-container {
49        width: 300px;
50        margin: 0px auto;
51        padding: 20px 30px;
52        background-color: #fff;
53        border-radius: 2px;
54        box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
55        transition: all .3s ease;
56        font-family: Helvetica, Arial, sans-serif;
57    }
58    .modal-header h3 {
59        margin-top: 0;
60        color: #42b983;
61    }
62    .modal-body {
63        margin: 20px 0;
64    }
65    .modal-default-button {
66        float: right;
67    }
68    .modal-enter {
69        opacity: 0;
70    }
71    .modal-leave-active {
72        opacity: 0;
73    }
74    .modal-enter .modal-container,
75    .modal-leave-active .modal-container {
76        -webkit-transform: scale(1.1);
77        transform: scale(1.1);
78    }
79    </style>

Then paste the following after the closing style tag:

1<script>
2    export default {
3        props: ['product'],
4        data() {
5            return {
6                attachment: null
7            }
8        },
9        computed: {
10            data: function() {
11                if (this.product != null) {
12                    return this.product
13                }
14                return {
15                    name: "",
16                    units: "",
17                    price: "",
18                    description: "",
19                    image: false
20                }
21            }
22        },
23        methods: {
24            attachFile(event) {
25                this.attachment = event.target.files[0];
26            },
27            uploadFile(event) {
28                if (this.attachment != null) {
29                    var formData = new FormData();
30                    formData.append("image", this.attachment)
31                    let headers = {'Content-Type': 'multipart/form-data'}
32                    axios.post("/api/upload-file", formData, {headers}).then(response => {
33                        this.product.image = response.data
34                        this.$emit('close', this.product)
35                    })
36                } else {
37                    this.$emit('close', this.product)
38                }
39            }
40        }
41    }
42    </script>

When the modal receives a product’s data, it pre-fills each field with the data. When we attach an image and submit the modal’s form, it is uploaded and the url for the image is returned to us.

We update the image attribute of the product with the url, then emit a close event and return the product with it. If no image is attached, we emit a close event and return the product data along with it.

? This modal component is an example provided in Vue documentation here

Payments

There are many payment options for e-commerce platforms. You choose depending on your needs and what is available in your country. Many popular payment processors like Stripe have excellent guides on integrating into a JavaScript or PHP application which you can use. This tutorial won’t cover that though, you can take it as an exercise for practise.

Building our application

We are done building our application. The next thing to do will be to compile our Vue application and serve our Laravel backend. Run the command to build the application.

1$ npm run prod

Then, run the command to serve our application.

1$ php artisan serve

Conclusion

In this series, we have used Laravel and Vue to build a simple e-commerce application. This guide provides a basic e-commerce application implementation and can form the starting blocks for a more robust application.

The code for the application is on GitHub.