In this tutorial, we are going to build an E-commerce store with Nuxt.js frontend, Strapi backend, Nodemailer for sending emails, and also integrate Stripe for payments. The E-commerce store we will build can be found here.
What you’ll need for this tutorial:
Table of Contents:
Here’s what the completed version of your application will look like.
Let’s get started.
The Strapi documentation says that Strapi is a flexible, open-source Headless CMS that allows developers the freedom to choose their favorite tools and frameworks while allowing editors to manage and distribute their content efficiently. Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences by making the admin panel and API extensible through a plugin system.
Strapi helps us build an API in no time; I mean no hassle of creating a server from scratch. With Strapi, we can do everything literally, and it’s easily customizable; we can add our code and edit functionalities easily. Strapi is fantastic, and I’m being modest about it. I’m still stunned by what Strapi can do.
Strapi provides an admin panel to edit and create APIs and includes code that can be edited. It’s very easy to edit the code and uses JavaScript.
To install Strapi, head over to the Strapi docs; we’ll be using the SQLite database for this project. To install Strapi, run the following commands:
1 yarn create strapi-app my-project # using yarn
2 npx create-strapi-app my-project # using npx
Replace my-project
with the name you wish to call your application directory, your package manager will create a directory with the name and will install Strapi.
If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following commands to start the Strapi development server:
1 yarn develop # using yarn
2 npm run develop # using npm
The development server starts our app on http://localhost:1337/admin.
We have Strapi up and running; the next step is to create our content-types of our application.
Save the content-types; now we can view our API in JSON format by visiting http://localhost:1337/api/products.
To install Nuxt.js, visit the Nuxt.js docs.
We want to use Nuxt in SSR mode, server hosting, and Tailwind CSS as our preferred CSS framework, so go ahead and select those, then choose the rest of the options according to your choice. Preferably leave out CI, commit-linting, style-linting, and the rest but do whatever you like; all we’ll need in this tutorial is what I’ve mentioned above.
To install Nuxt.js, run the following commands:
1 yarn create nuxt-app <project-name> # using yarn
2 npx create-nuxt-app <project-name> # using npx
3 npm init nuxt-app <project-name> # using npm
This will ask you questions (name, Nuxt options, UI framework, TypeScript, linter, testing framework, etc.)
Once all questions are answered, all the dependencies will be installed. The next step is to navigate to the project folder and launch it by running the following commands:
1 yarn dev # using yarn
2 npm run dev # using npm
We should have Nuxt running on http://localhost:3000.
Now, we can start building the frontend of our application. Let’s start by building our components, but first, let’s edit our layouts at layouts/default.vue
.
Head over to the layouts
directory and open the default.vue
file and fill it up with the following code:
1 <template>
2 <div>
3 <Nuxt />
4 </div>
5 </template>
6
7 <style>
8 html {
9 /* font-family: 'Nunito', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
10 Roboto, 'Helvetica Neue', Arial, sans-serif; */
11 font-family: 'Nunito';
12 font-size: 16px;
13 word-spacing: 1px;
14 -ms-text-size-adjust: 100%;
15 -webkit-text-size-adjust: 100%;
16 -moz-osx-font-smoothing: grayscale;
17 -webkit-font-smoothing: antialiased;
18 box-sizing: border-box;
19 }
20 *,
21 *::before,
22 *::after {
23 box-sizing: border-box;
24 margin: 0;
25 }
26 .button--hero {
27 display: inline-block;
28 color: #fff;
29 text-decoration: none;
30 padding: 10px 30px;
31 }
32 .button--green {
33 display: inline-block;
34 border: 1px solid #3b8070;
35 color: #3b8070;
36 text-decoration: none;
37 padding: 10px 30px;
38 }
39 .button--green:hover {
40 color: #fff;
41 background-color: #3b8070;
42 }
43 .button--grey {
44 display: inline-block;
45 border: 1px solid #35495e;
46 color: #35495e;
47 text-decoration: none;
48 padding: 10px 30px;
49 margin-left: 15px;
50 }
51 .button--grey:hover {
52 color: #fff;
53 background-color: #35495e;
54 }
55 .button--delete {
56 display: inline-block;
57 border: 1px solid #35495e;
58 padding: 5px;
59 color: white;
60 background-color: #35495e;
61 }
62 button:focus {
63 outline: none;
64 }
65 .container {
66 width: 80%;
67 }
68 </style>
Building the Hero Section
Execute the following code to create a HeroSection.vue
file.
1 cd components
2 touch HeroSection.vue
Fill up the HeroSection.vue
file with the code below.
1 <template>
2 <div
3 class="hero relative max-h-screen flex flex-col justify-center items-center text-center mx-auto bg-cover"
4 >
5 <div class="relative m-10 md:m-20">
6 <div class="relative">
7 <img class="absolute left-0 top-0" src="Repeat_Grid_2.png" alt="" />
8 </div>
9 <div class="z-10 relative">
10 <h1 class="text-5xl text-white m-3 font-bold md:text-6xl">
11 Unique Essence Store
12 </h1>
13 <p class="text-white subtitle">...your one stop shop for all</p>
14 </div>
15 <div class="circle absolute z-0 right-0 top-0"></div>
16 <div class="links mt-10">
17 <NuxtLink to="/" class="button--hero bg-button relative z-10">
18 View Collections
19 </NuxtLink>
20 </div>
21 </div>
22 </div>
23 </template>
24
25 <script>
26 export default {
27 name: 'HeroSection',
28 }
29 </script>
30
31 <style scoped>
32 /* Sample `apply` at-rules with Tailwind CSS
33 .container {
34 @apply min-h-screen flex justify-center items-center text-center mx-auto;
35 }
36 */
37 .hero {
38 background-color: #9c7474dc;
39 }
40 .circle {
41 width: 10em;
42 height: 10em;
43 border-radius: 5em;
44 background: #b8738d;
45 }
46 .title {
47 font-family: 'Nunito';
48 display: block;
49 font-weight: 700;
50 font-size: 50px;
51 letter-spacing: 1px;
52 line-height: 1em;
53 }
54 .subtitle {
55 font-weight: 100;
56 word-spacing: 2px;
57 }
58 </style>
Building the Ads Section
Still in the components
folder create an Ads.vue
file by running the following command:
1 touch Ads.vue
Then fill it up with the following code:
1 <template>
2 <div class="bg-primary ads flex justify-center items-center">
3 <h3 class="text-white text-lg ml-6 sm:text-2xl font-bold">
4 50% off on all Purchases Above $300, Hurry Now!!!!
5 </h3>
6 <img
7 class="h-48 sm:pl-20"
8 :src="`uriel-soberanes-MxVkWPiJALs-unsplash-removebg-preview.png`"
9 alt=""
10 />
11 </div>
12 </template>
13
14 <script>
15 export default {
16 name: 'Ads',
17 }
18 </script>
19
20 <style scpoed>
21 </style>
Here, all we’re doing is linking to an image from our static folder and displaying some promo messages.
Building the Footer Section
Execute the following code to create a Footer.vue
file.
1 touch Footer.vue
Fill it up with the following code:
1 <template>
2 <div
3 class="flex flex-col mt-10 sm:mt-0 sm:flex-row sm:items-center bg-primary text-white space-x-5 p-5 py-8 md:space-x-20 md:p-20 font-light text-sm"
4 >
5 <p class="ml-5 font-bold">Unique Essense Store</p>
6 <div class="m-2 mb-3">
7 <NuxtLink to="/" class="m-2"><p>Home</p></NuxtLink>
8 <NuxtLink to="/all" class="m-2"><p>All</p></NuxtLink>
9 <NuxtLink to="men" class="m-2"><p>Men</p></NuxtLink>
10 <NuxtLink to="women" class="m-2"><p>Women</p></NuxtLink>
11 </div>
12 <p>
13 Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium
14 doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo
15 inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
16 Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut
17 fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem
18 sequi nesciunt
19 </p>
20 </div>
21 </template>
22
23 <script>
24 export default {
25 name: 'Footer',
26 }
27 </script>
28
29 <style scoped>
30 </style>
Vuex is a state manager that is popularly used with Vue.js. We’ll be setting up a simple store with Vuex in this section.
Execute the following code to open up the index.js
file in the store
folder.
1 cd store
2 code index.js
Then fill up index.js
with the following code:
1 export const state = () => ({
2 allProducts: [],
3 featuredProducts: [],
4 menProducts: [],
5 womenProducts: [],
6 cartItems: [],
7 })
8
9 export const getters = {
10 /*
11 return items from store
12 */
13 allProducts: (state) => state.allProducts,
14 featuredProducts: (state) => state.featuredProducts,
15 menProducts: (state) => state.menProducts,
16 womenProducts: (state) => state.womenProducts,
17 getCart: (state) => state.cartItems,
18 getCartTotal: (state) =>
19 state.cartItems.length < 1
20 ? '0'
21 : state.cartItems
22 .map((el) => el.price * el.quantity)
23 .reduce((a, b) => a + b),
24 }
25
26 export const actions = {
27 async addItemToCart({ commit }, cartItem) {
28 await commit('setCartItem', cartItem)
29 },
30 async deleteCartItem({ commit }, id) {
31 await commit('removeCartItem', id)
32 },
33 }
34
35 export const mutations = {
36 setProducts: (state, products) => (state.allProducts = products),
37 setFeaturedProducts: (state, products) => (state.featuredProducts = products),
38 setMenProducts: (state, products) => (state.menProducts = products),
39 setWomenProducts: (state, products) => (state.womenProducts = products),
40 setCartItem: (state, item) => state.cartItems.push(item),
41 removeCartItem: (state, id) =>
42 state.cartItems.splice(
43 state.cartItems.findIndex((el) => el.id === id),
44 1
45 ),
46 }
We need some kind of alert system to improve the user experience of our website, for example, to alert users when they carry out a specific task. We’ll be using @nuxtjs/swal module for that purpose.
Execute the following code to install the package.
1 yarn add @nuxtjs/swal # using yarn
2 npm install @nuxtjs/swal # using npm
Then add the following code to your nuxt.config.js
file.
1 plugins: ['~/plugins/vue-swal'],
2
3 build: {
4 /*
5 ** Run ESLint on save
6 */
7 extend(config, ctx) {
8 if (ctx.dev && ctx.isClient) {
9 config.module.rules.push({
10 enforce: 'pre',
11 test: /\.(js|vue)$/,
12 loader: 'eslint-loader',
13 exclude: /(node_modules)/,
14 })
15 }
16 },
17 /*
18 ** Add vue-swal
19 */
20 vendor: ['vue-swal'],
21 },
We’ll be using @nuxtjs/strapi module for to make API calls to our Strapi
back-end.
Execute the following code to install the package.
1 yarn add @nuxtjs/strapi@^0.3.4 # using yarn
2 npm install @nuxtjs/strapi@^0.3.4 # using npm
Then add the following code to your nuxt.config.js
file:
1 modules: [
2 ...
3 '@nuxtjs/strapi',
4 ],
5
6 strapi: {
7 url: process.env.STRAPI_URL || 'http://localhost:1337/api',
8 entities: ['products', 'orders', 'subscribers'],
9 },
10
11 env: {
12 STRAPI_URL: `http://localhost:1337/api`,
13 },
Building the Newsletter Section
Execute the following code to create a NewsLetter.vue
file.
1 cd components
2 touch NewsLetter.vue
Fill it up with the code below:
1 <template>
2 <div
3 class="sm:flex mx-auto items-center m-10 justify-center space-x-6 sm:space-x-20 m-3 sm:m-6 mx-6"
4 >
5 <div>
6 <h1 class="text-lg m-7">Sign Up For NewsLetter</h1>
7 </div>
8 <div>
9 <form @submit="handleSuscribe">
10 <input
11 id=""
12 v-model="email"
13 class="p-2 m-3 sm:m-0 border border-solid border-t-0 border-l-0 border-r-0 border-b-1 outline-none border-black"
14 type="email"
15 name=""
16 placeholder="email"
17 />
18 <button type="submit" class="button--grey">Subscribe</button>
19 </form>
20 </div>
21 </div>
22 </template>
23
24 <script>
25 export default {
26 name: 'NewsLetter',
27 data() {
28 return {
29 email: '',
30 }
31 },
32 methods: {
33 async handleSuscribe(e) {
34 e.preventDefault()
35 this.$swal({
36 title: 'Successful!',
37 text: 'Thanks for Subscribing',
38 icon: 'success',
39 button: 'Ok',
40 })
41 await this.$strapi.$subscribers.create({ Email: this.email })
42 this.email = '' // clear email input
43 },
44 },
45 }
46 </script>
This newsletter section gives users the privilege to sign up for our e-commerce store’s newsletter. We use the @nuxtjs/strapi
module to create a new subscriber every time our handleSuscribe
method is invoked.
Building the Nav Section
Execute the following code to create a Nav.vue
file.
1 cd components
2 touch Nav.vue
Add the following code to the file:
1 <template>
2 <div class="navbar flex text-white relative">
3 <div class="nav-item-center flex space-x-6 sm:space-x-20 p-5 mx-auto">
4 <NuxtLink to="/">Home</NuxtLink>
5 <NuxtLink to="/all">All</NuxtLink>
6 <NuxtLink to="/men">Men</NuxtLink>
7 <NuxtLink to="/women">Women</NuxtLink>
8 </div>
9 <div class="cart fixed bottom-0 right-0 shadow-md m-3">
10 <p class="p-1 cartCount text-xs absolute top-0 right-0">
11 {{ getCart.length }}
12 </p>
13 <NuxtLink to="/cart">
14 <p class="pt-3 px-2">Cart</p>
15 </NuxtLink>
16 </div>
17 <!-- <div class="ham-menu shadow-md fixed bottom-0 right-0 m-3 sm:hidden">
18 <p class="text-center pt-3">menu</p>
19 </div> -->
20 </div>
21 </template>
22
23 <script>
24 import { mapGetters } from 'vuex'
25 export default {
26 name: 'Nav',
27 computed: {
28 ...mapGetters(['getCart']),
29 },
30 }
31 </script>
32
33 <style scoped>
34 .ham-menu {
35 background-color: #000;
36 width: 3em;
37 height: 3em;
38 border-radius: 1.5em;
39 }
40 .cart {
41 background-color: rgb(163, 87, 129);
42 width: 3em;
43 height: 3em;
44 border-radius: 1.5em;
45 }
46 .navbar {
47 background-color: rgb(24, 20, 22);
48 }
49 .cartCount {
50 background: #000;
51 border-radius: 30%;
52 }
53 </style>
In the Nav section component, we are making use of the Vuex store, we are using the getCart
getter to get the number of items added to the cart from the Vuex store.
Building the Featured section
Still in the components
folder, execute the following code to create a Featured.vue
file.
1 touch Featured.vue
Add the following lines of code to the Featured.vue
component:
1 <template>
2 <div>
3 <div
4 class="sm:grid sm:grid-cols-3 md:grid-cols-3 gap-6 justify-center items-center"
5 >
6 <div
7 v-for="(product, i) in data"
8 :key="i"
9 class="flex flex-col max-h-screen shadow-xl m-8 sm:m-2 md:m-4 justify-center items-center"
10 >
11 <!-- <div>{{ product }}</div> -->
12 <div class="img-wrapper h-3/4 mx-auto max-h-screen">
13 <img
14 class="flex-shrink h-1/2"
15 :src="`http://localhost:1337${product.attributes.image.data.attributes.formats.small.url}`"
16 alt=""
17 />
18 </div>
19 <div>
20 <p class="text-center m-3">
21 {{ product.attributes.name }}
22 </p>
23 <NuxtLink :to="`/products/${product.id}`">
24 <button class="button--green mb-4">View Product</button>
25 </NuxtLink>
26 </div>
27 </div>
28 </div>
29 </div>
30 </template>
31
32 <script>
33 export default {
34 name: 'Featured',
35 props: ['data'],
36 }
37 </script>
38
39 <style scoped>
40 </style>
What we’re doing here is displaying the data from our products API. This component is responsible for the cards that are displayed on the homepage, men and women products pages. We pass the data as props from the parent down to the featured
component, then loop through it and display the data properly. If you recall correctly, our products content-type has a name
and image
property.
Building the Products Section
This component is responsible for displaying the details about individual product details and it also provides the option to add a product to our cart.
Execute the following code to create a Products.vue
file.
1 touch Products.vue
Add the following lines of code:
1 <template>
2 <div>
3 <div
4 class="sm:grid grid-cols-2 justify-center shadow-lg items-center gap-3 m-5 md:m-5"
5 >
6 <div>
7 <img
8 class="max-h-screen"
9 :src="`http://localhost:1337${data.attributes.image.data.attributes.formats.small.url}`"
10 />
11 </div>
12 <div class="sm:m-3 md:m-5 p-3 sm:p-0">
13 <p class="my-2">
14 <span>Price: </span>{{ data.attributes.price | formatPrice }}
15 </p>
16 <span class="my-2">Quantity: </span
17 ><input
18 v-model="cartItem.quantity"
19 class="p-3 border border-solid border-t-0 border-l-0 border-r-0 border-b-1"
20 type="number"
21 />
22 <p class="my-2 text-sm">{{ data.attributes.description }}</p>
23 <button
24 class="button--green my-2"
25 @click="
26 addItemToCart(cartItem)
27 displayMessage()
28 "
29 >
30 Add to Cart
31 </button>
32 </div>
33 </div>
34 </div>
35 </template>
36
37 <script>
38 import { mapActions } from 'vuex'
39 export default {
40 name: 'Products',
41 props: ['data'],
42 data() {
43 return {
44 cartItem: {
45 id: this.data.id,
46 name: this.data.attributes.name,
47 url: `http://localhost:1337${this.data.attributes.image.data.attributes.formats.small.url}`,
48 price: this.data.attributes.price,
49 quantity: 1,
50 },
51 }
52 },
53 methods: {
54 ...mapActions(['addItemToCart']),
55 displayMessage() {
56 this.$swal({
57 title: 'Cart Updated!',
58 text: `${this.data.name} was added to your cart!`,
59 icon: 'success',
60 button: 'Ok',
61 })
62 },
63 },
64 filters: {
65 formatPrice(price) {
66 return `$${price}`
67 },
68 },
69 }
70 </script>
71
72 <style scoped>
73 </style>
That’s all for our components.
We can now start building our pages with Nuxt.js. Nuxt.js provides automatic page routing when we build pages. Amazing right!!.
Building our Homepage
Execute the following code to create an index.vue
file in the pages
directory.
1 cd pages
2 touch index.vue
Then we fill it up with the following code:
1 <template>
2 <div>
3 <HeroSection />
4 <Nav class="sticky top-0" />
5 <div class="sm:w-11/12 md:w-4/5 mx-auto">
6 <h1 class="m-5 font-bold text-lg">Featured Products</h1>
7 <Featured class="mx-auto" :data="featuredProducts" />
8 </div>
9 <Ads class="mx-auto sm:m-10" />
10 <NewsLetter class="mx-auto" />
11 <Footer />
12 </div>
13 </template>
14
15 <script>
16 import { mapGetters } from 'vuex'
17 export default {
18 async asyncData({ $strapi, $http, store, error }) {
19 try {
20 const response = await this.$strapi.$products.find({
21 featured: true,
22 populate: '*'
23 })
24
25 store.commit('setFeaturedProducts', response.data)
26 } catch (e) {
27 error(e)
28 }
29 },
30 data() {
31 return {
32 featuredProds: [],
33 }
34 },
35 computed: {
36 ...mapGetters(['featuredProducts']),
37 },
38 }
39 </script>
40
41 <style scoped>
42 /* Sample `apply` at-rules with Tailwind CSS
43 .container {
44 @apply min-h-screen flex justify-center items-center text-center mx-auto;
45 }
46 */
47 </style>
What we’re doing here is basically creating our homepage using the components we created earlier on. Furthermore, we’re using @nuxtjs/strapi
package to fetch data (featured products) from our Strapi backend API inside of our asyncData()
lifecycle method and then we commit the data to the Vuex store. Then we get data from the store to display on our homepage through our featuredProducts
getter.
Building All Products Page
Still in the pages
directory, execute the following code to create an all.vue
file.
1 touch all.vue
Fill the all.vue
file up with the following code:
1 <template>
2 <div>
3 <Nav class="sticky top-0" />
4 <div class="sm:w-11/12 md:w-4/5 mx-auto">
5 <h1 class="m-5 font-bold text-lg">Our Collection</h1>
6 <div class="flex justify-center text-center mx-auto">
7 <Featured :data="allProducts" />
8 </div>
9 </div>
10 <Ads class="mx-auto sm:m-10" />
11 <Footer />
12 </div>
13 </template>
14
15 <script>
16 import { mapGetters } from 'vuex'
17 export default {
18 async asyncData({ $strapi, $http, store, error }) {
19 try {
20 const response = await this.$strapi.$products.find({ populate: '*' })
21 store.commit('setProducts', response.data)
22 } catch (e) {
23 error(e)
24 }
25 },
26 data() {
27 return {
28 products: [],
29 }
30 },
31 computed: {
32 ...mapGetters(['allProducts']),
33 },
34 }
35 </script>
36
37 <style scoped></style>
Here, we’re doing the same thing as we did in the index.vue
page, the only difference is that we’re fetching all of our products from the Backend API.
Building the Men Products Page
Execute the following code to create a men.vue
file.
1 touch men.vue
Then proceed to fill it up with the following code:
1 <template>
2 <div>
3 <Nav class="sticky top-0" />
4 <div class="md:w-4/5 sm:w-11/12 mx-auto">
5 <h1 class="m-5 font-bold text-lg">Men's Collection</h1>
6 <div class="flex justify-center text-center mx-auto">
7 <Featured :data="menProducts" />
8 </div>
9 </div>
10 <Ads class="mx-auto sm:m-10" />
11 <Footer />
12 </div>
13 </template>
14
15 <script>
16 import { mapGetters } from 'vuex'
17 export default {
18 async asyncData({ $strapi, $http, store, error }) {
19 try {
20 let response = await this.$strapi.$products.find({ populate: '*' })
21 response = response.data.filter(
22 (el) => el.attributes.category.data.attributes.name === 'men'
23 )
24 store.commit('setMenProducts', response)
25 } catch (e) {
26 error(e)
27 }
28 },
29 data() {
30 return {
31 menProds: [],
32 }
33 },
34 computed: {
35 ...mapGetters(['menProducts']),
36 },
37 }
38 </script>
39
40 <style scoped>
41 </style>
Building Women Products Page
Execute the following code to create a women.vue
file.
1 touch women.vue
Then proceed to fill it up with the following code.
1 <template>
2 <div>
3 <Nav class="sticky top-0" />
4 <div class="sm:w-11/12 md:w-4/5 mx-auto">
5 <h1 class="m-5 font-bold text-lg">Women's Collection</h1>
6 <div class="flex justify-center text-center mx-auto">
7 <Featured :data="womenProducts" />
8 </div>
9 </div>
10 <Ads class="mx-auto sm:m-10" />
11 <Footer />
12 </div>
13 </template>
14
15 <script>
16 import { mapGetters } from 'vuex'
17 export default {
18 async asyncData({ $strapi, $http, store, error }) {
19 try {
20 let response = await this.$strapi.$products.find({ populate: '*' })
21 response = response.data.filter(
22 (el) => el.attributes.category.data.attributes.name === 'women'
23 )
24 store.commit('setWomenProducts', response)
25 } catch (e) {
26 error(e)
27 }
28 },
29 data() {
30 return {
31 womenProds: [],
32 }
33 },
34 computed: {
35 ...mapGetters(['womenProducts']),
36 },
37 }
38 </script>
39
40 <style scoped>
41 </style>
Building Product Detail Page
Execute the following code to create a _products.vue
file.
1 mkdir products
2 touch _products.vue
Fill it up with the following code:
1 <template>
2 <div>
3 <Nav class="sticky top-0" />
4 <h1 class="font-bold m-5 md:mx-10">
5 {{ currentProduct.attributes.name }}
6 </h1>
7 <Products :data="currentProduct" />
8 <Ads class="mx-auto sm:m-10" />
9 <Footer />
10 </div>
11 </template>
12
13 <script>
14 export default {
15 async asyncData({ $strapi, $http, route }) {
16 const id = route.params.products
17 const response = await this.$strapi.$products.findOne(id, { populate: '*' })
18 const { data: currentProduct } = response
19 return { currentProduct }
20 },
21 data() {
22 return {
23 currentProduct: {},
24 }
25 },
26 }
27 </script>
28
29 <style scoped>
30 </style>
Building Cart Page
Execute the following code to create a cart.vue
file.
1 touch cart.vue
Then fill the file up with the following code:
1 <template>
2 <div>
3 <Nav class="sticky top-0" />
4 <div class="w-4/5 sm:w-1/2 mx-auto">
5 <h1 class="m-5 font-bold text-lg">Your Cart</h1>
6 </div>
7 <div
8 v-for="item in getCart"
9 :key="item.id"
10 class="w-4/5 sm:w-1/2 flex items-center space-x-3 mx-auto shadow-lg m-5 p-3"
11 >
12 <div>
13 <img class="h-24" :src="`${item.url}`" alt="" />
14 </div>
15 <div>
16 <p>
17 {{ item.name }}
18 </p>
19 <p>
20 {{ item.quantity | formatQuantity }}
21 </p>
22 <button class="button--delete" @click="deleteCartItem(item.id)">
23 Delete
24 </button>
25 </div>
26 </div>
27 <div class="w-4/5 sm:w-1/2 mb-2 mx-auto">
28 <p>
29 <span>Total: </span> {{ formatCartTotal(getCartTotal) | formatPrice }}
30 </p>
31 <button
32 v-show="getCartTotal > 0"
33 class="button--green mx-auto"
34 @click="handleSubmit"
35 >
36 checkout
37 </button>
38 </div>
39 <Ads class="mx-auto sm:m-10" />
40 <Footer />
41 </div>
42 </template>
43
44 <script>
45 import { mapGetters, mapActions } from 'vuex'
46 export default {
47 data() {},
48 computed: {
49 ...mapGetters(['getCart', 'getCartTotal']),
50 },
51 methods: {
52 async handleSubmit(e) {
53 },
54 formatCartTotal(num) {
55 if (num > 0) {
56 return num.toFixed(2)
57 } else {
58 return num
59 }
60 },
61 ...mapActions(['deleteCartItem']),
62 },
63 filters: {
64 formatPrice(price) {
65 return `$${price}`
66 },
67 formatQuantity(num) {
68 const qtyNum = num === 1 ? `${num} unit` : `${num} units`
69 return qtyNum
70 },
71 },
72 }
73 </script>
74
75 <style scoped>
76 </style>
We just built our cart page and if you look at the code above closely, you’ll notice some methods that we’ve not defined yet. In the next section, we’ll define them when we integrate Stripe payments.
To get started with stripe, go to Stripe, and register to obtain your API keys before proceeding with this tutorial.
Once you’ve done that, I’ll assume you’ve gotten your API keys and you’re ready to proceed.
Installing Stripe Package
Execute the following code to install Stripe.
1 yarn add stripe # using yarn
2 npm install stripe # using npm
Look for the .env.example
file in the root folder of your Strapi application and rename it to .env
. Then add the following line of text to it:
1STRIPE_KEY=<YOUR_STRIPE_KEY>
Replace <YOUR_STRIPE_KEY>
with your Stripe credentials.
Then proceed as follows to open up the order.js
controller.
1 cd src/api
2 cd order
3 cd controllers
4 code order.js # open in your editor
Edit the contents of order.js
to look like:
1 'use strict';
2
3 const stripe = require('stripe')(process.env.STRIPE_KEY)
4
5 const MY_DOMAIN = 'http://localhost:3000/cart';
6 const { createCoreController } = require('@strapi/strapi').factories;
7 module.exports = createCoreController('api::order.order', ({ strapi }) => ({
8 async create(ctx) {
9 const { cartDetail, cartTotal } = ctx.request.body
10 // build line items array
11 const line_items = cartDetail.map((cartItem) => {
12 const item = {}
13 item.price_data = {
14 currency: 'usd',
15 product_data: {
16 name: cartItem.name,
17 images: [`${cartItem.url}`]
18 },
19 unit_amount: (cartItem.price * 100).toFixed(0),
20 },
21 item.quantity = cartItem.quantity
22 return item;
23 })
24 // create order
25 await strapi.service('api::order.order').create({ data: {
26 item: line_items}});
27 const session = await stripe.checkout.sessions.create({
28 payment_method_types: ['card'],
29 line_items,
30 mode: 'payment',
31 success_url: `${MY_DOMAIN}?success=true`,
32 cancel_url: `${MY_DOMAIN}?canceled=true`,
33 })
34 return { id: session.id}
35 }
36 }));
What we’ve done here is to redefine the create
endpoint. Now when we hit the /order/create
endpoint, we generate a line_items
array which is stored in our database and also sent to Stripe as product details along with other essential details related to purchases. Finally, we return a JSON object containing an id
with the Stripe session ID.
That’s all for Stripe on the backend. Next, open your Nuxt.js application to add Stripe support on the frontend.
Installing @stripe/stripe-js
To start using Stripe in our Nuxt.js application, we have to install a package to help us make the process easier. Execute the following code to install @stripe/stripe-js.
1 yarn add @stripe/stripe-js # usnig yarn
2 npm install @stripe/stripe-js # using npm
Open up your cart.vue
file, then add the following lines of code to the file:
1 export default {
2 data() {
3 return {
4 dataItems: {},
5 session: {},
6 stripe: {},
7 stripePromise: {},
8 }
9 },
10 computed: {
11 ...mapGetters(['getCart', 'getCartTotal']),
12 },
13 mounted() {
14 this.displayMessage()
15 },
16 methods: {
17 async handleSubmit(e) {
18 e.preventDefault()
19 const response = await this.$http.$post(
20 `http://localhost:1337/api/orders`,
21 {
22 cartDetail: this.getCart,
23 cartTotal: this.getCartTotal.toFixed(2),
24 }
25 )
26 this.$swal({
27 title: 'Please wait',
28 text: 'redirecting you to stripe, click ok',
29 icon: 'success',
30 button: 'Ok',
31 })
32 // stripe logic
33 const stripePromise = loadStripe(process.env.STRIPE_KEY)
34 const session = response
35 const stripe = await stripePromise
36 const result = await stripe.redirectToCheckout({
37 sessionId: session.id,
38 })
39 console.log(response)
40 if (result.error) {
41 this.$nuxt.context.error(result.error.message)
42 }
43 },
44 // using vue-swal to display messages
45 displayMessage() {
46 if (this.$route.query.success) {
47 this.$swal({
48 title: 'Order placed!',
49 text: 'Thanks for placing your orders',
50 icon: 'success',
51 button: 'Ok',
52 })
53 } else if (this.$route.query.canceled) {
54 this.$swal({
55 title: 'Order canceled!',
56 text: "continue to shop around and checkout when you're ready.",
57 icon: 'warning',
58 button: 'Ok',
59 })
60 }
61 },
62 formatCartTotal(num) {
63 if (num > 0) {
64 return num.toFixed(2)
65 } else {
66 return num
67 }
68 },
69 ...mapActions(['deleteCartItem']),
70 },
71 filters: {
72 formatPrice(price) {
73 return `$${price}`
74 },
75 formatQuantity(num) {
76 const qtyNum = num === 1 ? `${num} unit` : `${num} units`
77 return qtyNum
78 },
79 },
80 }
Replace <YOUR_STRIPE_KEY>
with your Stripe credentials. Now, we should have Stripe working across the whole application.
To install Nodemailer, run:
1 yarn add nodemailer # using yarn
2 npm install nodemailer # using npm
Open up your Strapi application and execute the following code to access the subscriber.js
controller.
1 cd src/api
2 cd subscriber
3 cd controllers
4 code subscriber.js
Add the following lines of code to your subscriber.js
file:
1 'use strict';
2
3 const nodemailer = require('nodemailer')
4
5 module.exports = {
6 async create(ctx) {
7 const { Email } = ctx.request.body
8 const existingSub = await strapi.services.subscriber.find({ Email })
9 if (!existingSub) {
10 await strapi.services.subscriber.create({ Email })
11 try {
12 let transporter = nodemailer.createTransport({
13 service: "gmail",
14 auth: {
15 user: <your_email>,
16 pass: <your_password>,
17 }
18 })
19 const mailOptions = {
20 from: 'Unique essense stores',
21 to: `${Email}`,
22 subject: 'Welcome',
23 text: `Hey @${Email}, Thanks for subscribing to our NewsLetter`
24 };
25 await transporter.sendMail(mailOptions)
26 } catch (error) {
27 console.log(error)
28 }
29 }
30 return Email
31 }
32 };
In the code above, using Nodemailer, we’ve set up an email service that sends out notifications to subscribers.
In order for Nodemailer to work with Gmail, you need to turn on less secured app access. You can do so here.
Well devs, that’s all for now. I hope this tutorial has given you an insight into how to build your own E-Commerce store with Strapi. You could even add more features to your store if you like.
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)