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