Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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
2
yarn create strapi-app my-project # using yarn
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
2
yarn develop # using yarn
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
2
3
yarn create nuxt-app <project-name> # using yarn
npx create-nuxt-app <project-name> # using npx
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
2
yarn dev # using yarn
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
<div>
<Nuxt />
</div>
</template>
<style>
html {
/* font-family: 'Nunito', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif; */
font-family: 'Nunito';
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
.button--hero {
display: inline-block;
color: #fff;
text-decoration: none;
padding: 10px 30px;
}
.button--green {
display: inline-block;
border: 1px solid #3b8070;
color: #3b8070;
text-decoration: none;
padding: 10px 30px;
}
.button--green:hover {
color: #fff;
background-color: #3b8070;
}
.button--grey {
display: inline-block;
border: 1px solid #35495e;
color: #35495e;
text-decoration: none;
padding: 10px 30px;
margin-left: 15px;
}
.button--grey:hover {
color: #fff;
background-color: #35495e;
}
.button--delete {
display: inline-block;
border: 1px solid #35495e;
padding: 5px;
color: white;
background-color: #35495e;
}
button:focus {
outline: none;
}
.container {
width: 80%;
}
</style>
Building the Hero Section
Execute the following code to create a HeroSection.vue
file.
1
2
cd components
touch HeroSection.vue
Fill up the HeroSection.vue
file with the code below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
<div
class="hero relative max-h-screen flex flex-col justify-center items-center text-center mx-auto bg-cover"
>
<div class="relative m-10 md:m-20">
<div class="relative">
<img class="absolute left-0 top-0" src="Repeat_Grid_2.png" alt="" />
</div>
<div class="z-10 relative">
<h1 class="text-5xl text-white m-3 font-bold md:text-6xl">
Unique Essence Store
</h1>
<p class="text-white subtitle">...your one stop shop for all</p>
</div>
<div class="circle absolute z-0 right-0 top-0"></div>
<div class="links mt-10">
<NuxtLink to="/" class="button--hero bg-button relative z-10">
View Collections
</NuxtLink>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HeroSection',
}
</script>
<style scoped>
/* Sample `apply` at-rules with Tailwind CSS
.container {
@apply min-h-screen flex justify-center items-center text-center mx-auto;
}
*/
.hero {
background-color: #9c7474dc;
}
.circle {
width: 10em;
height: 10em;
border-radius: 5em;
background: #b8738d;
}
.title {
font-family: 'Nunito';
display: block;
font-weight: 700;
font-size: 50px;
letter-spacing: 1px;
line-height: 1em;
}
.subtitle {
font-weight: 100;
word-spacing: 2px;
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="bg-primary ads flex justify-center items-center">
<h3 class="text-white text-lg ml-6 sm:text-2xl font-bold">
50% off on all Purchases Above $300, Hurry Now!!!!
</h3>
<img
class="h-48 sm:pl-20"
:src="`uriel-soberanes-MxVkWPiJALs-unsplash-removebg-preview.png`"
alt=""
/>
</div>
</template>
<script>
export default {
name: 'Ads',
}
</script>
<style scpoed>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div
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"
>
<p class="ml-5 font-bold">Unique Essense Store</p>
<div class="m-2 mb-3">
<NuxtLink to="/" class="m-2"><p>Home</p></NuxtLink>
<NuxtLink to="/all" class="m-2"><p>All</p></NuxtLink>
<NuxtLink to="men" class="m-2"><p>Men</p></NuxtLink>
<NuxtLink to="women" class="m-2"><p>Women</p></NuxtLink>
</div>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium
doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo
inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut
fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem
sequi nesciunt
</p>
</div>
</template>
<script>
export default {
name: 'Footer',
}
</script>
<style scoped>
</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
2
cd store
code index.js
Then fill up index.js
with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export const state = () => ({
allProducts: [],
featuredProducts: [],
menProducts: [],
womenProducts: [],
cartItems: [],
})
export const getters = {
/*
return items from store
*/
allProducts: (state) => state.allProducts,
featuredProducts: (state) => state.featuredProducts,
menProducts: (state) => state.menProducts,
womenProducts: (state) => state.womenProducts,
getCart: (state) => state.cartItems,
getCartTotal: (state) =>
state.cartItems.length < 1
? '0'
: state.cartItems
.map((el) => el.price * el.quantity)
.reduce((a, b) => a + b),
}
export const actions = {
async addItemToCart({ commit }, cartItem) {
await commit('setCartItem', cartItem)
},
async deleteCartItem({ commit }, id) {
await commit('removeCartItem', id)
},
}
export const mutations = {
setProducts: (state, products) => (state.allProducts = products),
setFeaturedProducts: (state, products) => (state.featuredProducts = products),
setMenProducts: (state, products) => (state.menProducts = products),
setWomenProducts: (state, products) => (state.womenProducts = products),
setCartItem: (state, item) => state.cartItems.push(item),
removeCartItem: (state, id) =>
state.cartItems.splice(
state.cartItems.findIndex((el) => el.id === id),
1
),
}
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
2
yarn add @nuxtjs/swal # using yarn
npm install @nuxtjs/swal # using npm
Then add the following code to your nuxt.config.js
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
plugins: ['~/plugins/vue-swal'],
build: {
/*
** Run ESLint on save
*/
extend(config, ctx) {
if (ctx.dev && ctx.isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/,
})
}
},
/*
** Add vue-swal
*/
vendor: ['vue-swal'],
},
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
2
yarn add @nuxtjs/strapi@^0.3.4 # using yarn
npm install @nuxtjs/strapi@^0.3.4 # using npm
Then add the following code to your nuxt.config.js
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
modules: [
...
'@nuxtjs/strapi',
],
strapi: {
url: process.env.STRAPI_URL || 'http://localhost:1337/api',
entities: ['products', 'orders', 'subscribers'],
},
env: {
STRAPI_URL: `http://localhost:1337/api`,
},
Building the Newsletter Section
Execute the following code to create a NewsLetter.vue
file.
1
2
cd components
touch NewsLetter.vue
Fill it up with the code below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<div
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"
>
<div>
<h1 class="text-lg m-7">Sign Up For NewsLetter</h1>
</div>
<div>
<form @submit="handleSuscribe">
<input
id=""
v-model="email"
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"
type="email"
name=""
placeholder="email"
/>
<button type="submit" class="button--grey">Subscribe</button>
</form>
</div>
</div>
</template>
<script>
export default {
name: 'NewsLetter',
data() {
return {
email: '',
}
},
methods: {
async handleSuscribe(e) {
e.preventDefault()
this.$swal({
title: 'Successful!',
text: 'Thanks for Subscribing',
icon: 'success',
button: 'Ok',
})
await this.$strapi.$subscribers.create({ Email: this.email })
this.email = '' // clear email input
},
},
}
</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
2
cd components
touch Nav.vue
Add the following code to the file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<div class="navbar flex text-white relative">
<div class="nav-item-center flex space-x-6 sm:space-x-20 p-5 mx-auto">
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/all">All</NuxtLink>
<NuxtLink to="/men">Men</NuxtLink>
<NuxtLink to="/women">Women</NuxtLink>
</div>
<div class="cart fixed bottom-0 right-0 shadow-md m-3">
<p class="p-1 cartCount text-xs absolute top-0 right-0">
{{ getCart.length }}
</p>
<NuxtLink to="/cart">
<p class="pt-3 px-2">Cart</p>
</NuxtLink>
</div>
<!-- <div class="ham-menu shadow-md fixed bottom-0 right-0 m-3 sm:hidden">
<p class="text-center pt-3">menu</p>
</div> -->
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Nav',
computed: {
...mapGetters(['getCart']),
},
}
</script>
<style scoped>
.ham-menu {
background-color: #000;
width: 3em;
height: 3em;
border-radius: 1.5em;
}
.cart {
background-color: rgb(163, 87, 129);
width: 3em;
height: 3em;
border-radius: 1.5em;
}
.navbar {
background-color: rgb(24, 20, 22);
}
.cartCount {
background: #000;
border-radius: 30%;
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div>
<div
class="sm:grid sm:grid-cols-3 md:grid-cols-3 gap-6 justify-center items-center"
>
<div
v-for="(product, i) in data"
:key="i"
class="flex flex-col max-h-screen shadow-xl m-8 sm:m-2 md:m-4 justify-center items-center"
>
<!-- <div>{{ product }}</div> -->
<div class="img-wrapper h-3/4 mx-auto max-h-screen">
<img
class="flex-shrink h-1/2"
:src="`http://localhost:1337${product.attributes.image.data.attributes.formats.small.url}`"
alt=""
/>
</div>
<div>
<p class="text-center m-3">
{{ product.attributes.name }}
</p>
<NuxtLink :to="`/products/${product.id}`">
<button class="button--green mb-4">View Product</button>
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Featured',
props: ['data'],
}
</script>
<style scoped>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<template>
<div>
<div
class="sm:grid grid-cols-2 justify-center shadow-lg items-center gap-3 m-5 md:m-5"
>
<div>
<img
class="max-h-screen"
:src="`http://localhost:1337${data.attributes.image.data.attributes.formats.small.url}`"
/>
</div>
<div class="sm:m-3 md:m-5 p-3 sm:p-0">
<p class="my-2">
<span>Price: </span>{{ data.attributes.price | formatPrice }}
</p>
<span class="my-2">Quantity: </span
><input
v-model="cartItem.quantity"
class="p-3 border border-solid border-t-0 border-l-0 border-r-0 border-b-1"
type="number"
/>
<p class="my-2 text-sm">{{ data.attributes.description }}</p>
<button
class="button--green my-2"
@click="
addItemToCart(cartItem)
displayMessage()
"
>
Add to Cart
</button>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Products',
props: ['data'],
data() {
return {
cartItem: {
id: this.data.id,
name: this.data.attributes.name,
url: `http://localhost:1337${this.data.attributes.image.data.attributes.formats.small.url}`,
price: this.data.attributes.price,
quantity: 1,
},
}
},
methods: {
...mapActions(['addItemToCart']),
displayMessage() {
this.$swal({
title: 'Cart Updated!',
text: `${this.data.name} was added to your cart!`,
icon: 'success',
button: 'Ok',
})
},
},
filters: {
formatPrice(price) {
return `$${price}`
},
},
}
</script>
<style scoped>
</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
2
cd pages
touch index.vue
Then we fill it up with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<div>
<HeroSection />
<Nav class="sticky top-0" />
<div class="sm:w-11/12 md:w-4/5 mx-auto">
<h1 class="m-5 font-bold text-lg">Featured Products</h1>
<Featured class="mx-auto" :data="featuredProducts" />
</div>
<Ads class="mx-auto sm:m-10" />
<NewsLetter class="mx-auto" />
<Footer />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
async asyncData({ $strapi, $http, store, error }) {
try {
const response = await this.$strapi.$products.find({
featured: true,
populate: '*'
})
store.commit('setFeaturedProducts', response.data)
} catch (e) {
error(e)
}
},
data() {
return {
featuredProds: [],
}
},
computed: {
...mapGetters(['featuredProducts']),
},
}
</script>
<style scoped>
/* Sample `apply` at-rules with Tailwind CSS
.container {
@apply min-h-screen flex justify-center items-center text-center mx-auto;
}
*/
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div>
<Nav class="sticky top-0" />
<div class="sm:w-11/12 md:w-4/5 mx-auto">
<h1 class="m-5 font-bold text-lg">Our Collection</h1>
<div class="flex justify-center text-center mx-auto">
<Featured :data="allProducts" />
</div>
</div>
<Ads class="mx-auto sm:m-10" />
<Footer />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
async asyncData({ $strapi, $http, store, error }) {
try {
const response = await this.$strapi.$products.find({ populate: '*' })
store.commit('setProducts', response.data)
} catch (e) {
error(e)
}
},
data() {
return {
products: [],
}
},
computed: {
...mapGetters(['allProducts']),
},
}
</script>
<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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div>
<Nav class="sticky top-0" />
<div class="md:w-4/5 sm:w-11/12 mx-auto">
<h1 class="m-5 font-bold text-lg">Men's Collection</h1>
<div class="flex justify-center text-center mx-auto">
<Featured :data="menProducts" />
</div>
</div>
<Ads class="mx-auto sm:m-10" />
<Footer />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
async asyncData({ $strapi, $http, store, error }) {
try {
let response = await this.$strapi.$products.find({ populate: '*' })
response = response.data.filter(
(el) => el.attributes.category.data.attributes.name === 'men'
)
store.commit('setMenProducts', response)
} catch (e) {
error(e)
}
},
data() {
return {
menProds: [],
}
},
computed: {
...mapGetters(['menProducts']),
},
}
</script>
<style scoped>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div>
<Nav class="sticky top-0" />
<div class="sm:w-11/12 md:w-4/5 mx-auto">
<h1 class="m-5 font-bold text-lg">Women's Collection</h1>
<div class="flex justify-center text-center mx-auto">
<Featured :data="womenProducts" />
</div>
</div>
<Ads class="mx-auto sm:m-10" />
<Footer />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
async asyncData({ $strapi, $http, store, error }) {
try {
let response = await this.$strapi.$products.find({ populate: '*' })
response = response.data.filter(
(el) => el.attributes.category.data.attributes.name === 'women'
)
store.commit('setWomenProducts', response)
} catch (e) {
error(e)
}
},
data() {
return {
womenProds: [],
}
},
computed: {
...mapGetters(['womenProducts']),
},
}
</script>
<style scoped>
</style>
Building Product Detail Page
Execute the following code to create a _products.vue
file.
1
2
mkdir products
touch _products.vue
Fill it up with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<Nav class="sticky top-0" />
<h1 class="font-bold m-5 md:mx-10">
{{ currentProduct.attributes.name }}
</h1>
<Products :data="currentProduct" />
<Ads class="mx-auto sm:m-10" />
<Footer />
</div>
</template>
<script>
export default {
async asyncData({ $strapi, $http, route }) {
const id = route.params.products
const response = await this.$strapi.$products.findOne(id, { populate: '*' })
const { data: currentProduct } = response
return { currentProduct }
},
data() {
return {
currentProduct: {},
}
},
}
</script>
<style scoped>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<template>
<div>
<Nav class="sticky top-0" />
<div class="w-4/5 sm:w-1/2 mx-auto">
<h1 class="m-5 font-bold text-lg">Your Cart</h1>
</div>
<div
v-for="item in getCart"
:key="item.id"
class="w-4/5 sm:w-1/2 flex items-center space-x-3 mx-auto shadow-lg m-5 p-3"
>
<div>
<img class="h-24" :src="`${item.url}`" alt="" />
</div>
<div>
<p>
{{ item.name }}
</p>
<p>
{{ item.quantity | formatQuantity }}
</p>
<button class="button--delete" @click="deleteCartItem(item.id)">
Delete
</button>
</div>
</div>
<div class="w-4/5 sm:w-1/2 mb-2 mx-auto">
<p>
<span>Total: </span> {{ formatCartTotal(getCartTotal) | formatPrice }}
</p>
<button
v-show="getCartTotal > 0"
class="button--green mx-auto"
@click="handleSubmit"
>
checkout
</button>
</div>
<Ads class="mx-auto sm:m-10" />
<Footer />
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
data() {},
computed: {
...mapGetters(['getCart', 'getCartTotal']),
},
methods: {
async handleSubmit(e) {
},
formatCartTotal(num) {
if (num > 0) {
return num.toFixed(2)
} else {
return num
}
},
...mapActions(['deleteCartItem']),
},
filters: {
formatPrice(price) {
return `$${price}`
},
formatQuantity(num) {
const qtyNum = num === 1 ? `${num} unit` : `${num} units`
return qtyNum
},
},
}
</script>
<style scoped>
</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
2
yarn add stripe # using yarn
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:
1
STRIPE_KEY=<YOUR_STRIPE_KEY>
Replace <YOUR_STRIPE_KEY>
with your Stripe credentials.
Then proceed as follows to open up the order.js
controller.
1
2
3
4
cd src/api
cd order
cd controllers
code order.js # open in your editor
Edit the contents of order.js
to look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use strict';
const stripe = require('stripe')(process.env.STRIPE_KEY)
const MY_DOMAIN = 'http://localhost:3000/cart';
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::order.order', ({ strapi }) => ({
async create(ctx) {
const { cartDetail, cartTotal } = ctx.request.body
// build line items array
const line_items = cartDetail.map((cartItem) => {
const item = {}
item.price_data = {
currency: 'usd',
product_data: {
name: cartItem.name,
images: [`${cartItem.url}`]
},
unit_amount: (cartItem.price * 100).toFixed(0),
},
item.quantity = cartItem.quantity
return item;
})
// create order
await strapi.service('api::order.order').create({ data: {
item: line_items}});
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items,
mode: 'payment',
success_url: `${MY_DOMAIN}?success=true`,
cancel_url: `${MY_DOMAIN}?canceled=true`,
})
return { id: session.id}
}
}));
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
2
yarn add @stripe/stripe-js # usnig yarn
npm install @stripe/stripe-js # using npm
Open up your cart.vue
file, then add the following lines of code to the file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
export default {
data() {
return {
dataItems: {},
session: {},
stripe: {},
stripePromise: {},
}
},
computed: {
...mapGetters(['getCart', 'getCartTotal']),
},
mounted() {
this.displayMessage()
},
methods: {
async handleSubmit(e) {
e.preventDefault()
const response = await this.$http.$post(
`http://localhost:1337/api/orders`,
{
cartDetail: this.getCart,
cartTotal: this.getCartTotal.toFixed(2),
}
)
this.$swal({
title: 'Please wait',
text: 'redirecting you to stripe, click ok',
icon: 'success',
button: 'Ok',
})
// stripe logic
const stripePromise = loadStripe(process.env.STRIPE_KEY)
const session = response
const stripe = await stripePromise
const result = await stripe.redirectToCheckout({
sessionId: session.id,
})
console.log(response)
if (result.error) {
this.$nuxt.context.error(result.error.message)
}
},
// using vue-swal to display messages
displayMessage() {
if (this.$route.query.success) {
this.$swal({
title: 'Order placed!',
text: 'Thanks for placing your orders',
icon: 'success',
button: 'Ok',
})
} else if (this.$route.query.canceled) {
this.$swal({
title: 'Order canceled!',
text: "continue to shop around and checkout when you're ready.",
icon: 'warning',
button: 'Ok',
})
}
},
formatCartTotal(num) {
if (num > 0) {
return num.toFixed(2)
} else {
return num
}
},
...mapActions(['deleteCartItem']),
},
filters: {
formatPrice(price) {
return `$${price}`
},
formatQuantity(num) {
const qtyNum = num === 1 ? `${num} unit` : `${num} units`
return qtyNum
},
},
}
Replace <YOUR_STRIPE_KEY>
with your Stripe credentials. Now, we should have Stripe working across the whole application.
To install Nodemailer, run:
1
2
yarn add nodemailer # using yarn
npm install nodemailer # using npm
Open up your Strapi application and execute the following code to access the subscriber.js
controller.
1
2
3
4
cd src/api
cd subscriber
cd controllers
code subscriber.js
Add the following lines of code to your subscriber.js
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'use strict';
const nodemailer = require('nodemailer')
module.exports = {
async create(ctx) {
const { Email } = ctx.request.body
const existingSub = await strapi.services.subscriber.find({ Email })
if (!existingSub) {
await strapi.services.subscriber.create({ Email })
try {
let transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: <your_email>,
pass: <your_password>,
}
})
const mailOptions = {
from: 'Unique essense stores',
to: `${Email}`,
subject: 'Welcome',
text: `Hey @${Email}, Thanks for subscribing to our NewsLetter`
};
await transporter.sendMail(mailOptions)
} catch (error) {
console.log(error)
}
}
return Email
}
};
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)