Registration and Login (Authentication) with Vue.js and Strapi

Prerequisites

  • Basic knowledge of Vue.js
  • Knowledge of JavaScript
  • Node.js (v14 recommended for Strapi)

What You Will Learn

What is Strapi?

Installing Strapi

yarn create strapi-app my-project //using yarn
npx create-strapi-app my-project --quickstart //using npm
yarn develop //using yarn
npm run develop //using npm

Building our Bookmarks Collection Type

  1. Open up the Strapi admin panel.
  2. Navigate to the Content-Type Builder section
  3. Under Collection Types, click “create new collection type.”
  4. A popup window should come up and prompt you to enter a display name, type bookmark and then click Continue.
  5. Another popup should come up where you can choose the fields you want the Collection-Type to have.
  1. On the popup window, click Text, name the field label, leave the type selection as Short Text, and add another field.
  • Select Text, name the field source, leave the type selection as Short Text, and click on add another field.
  • Select Text, name the field image, leave the type selection as Short Text, and click on add another field.
  • Select Text, name the field url, leave the type selection as Short Text, and next, add another field.
  • Select Number, name the field yield, set the Number format selection as Integer, and next, add another field.
  • Select Number, name the field totalTime, set the Number format selection as decimal, and next, add another field.
  1. Select JSON, name the field ingredientLines, then click on add another field.
  2. Select Relations, and then click on the dropdown on the right side of the popup window, select User (from: users-permissions-user), then click on Users have many bookmarks. It should look like the image below.
  1. Click on the Finish.
  1. Finally, click on the Save button.

Setting the Permissions for Authenticated Users

  1. Click on Settings under GENERAL in the side menu
  2. Click on Roles under Users and Permissions Plugin.
  3. It will display a list of roles. Click on Authenticated
  4. Scroll down, under Permissions, click on Bookmark, then check the Create, findOne, and find checkboxes.
  5. Click save, then go back.

Setting the Permissions for Public Users

  1. Click public
  2. Scroll down, click on users-permissions under permissions, then check all the checkboxes.
  3. Click save, then go back.

Password Recovery Setup

  1. Under Users & Permission plugin, click on Advanced settings.
  2. Fill up the Reset password page input with the following url, http://localhost:8080/resetpassword.
  3. Click save, then go back.

Getting our Edamam Recipe API Credentials

Installing Vue.js

npm install -g @vue/cli 
# OR
yarn global add @vue/cli
vue create my-project
cd my-project
yarn serve //using yarn
npm serve //using npm

Integrating TailwindCss with Vue.js

  • Run this command:
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
or
yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
  • Then in the root of your Vue.js folder, create a postcss.config.js and fill it up with the following lines.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
  • Also, in the root of the Vue.js folder create a tailwindcss.config.js and fill it up with the following lines.
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
fontFamily: {
'pacifico': ['Pacifico'],
'montserrat': ['Montserrat'],
'roboto': ['Roboto'],
'righteous': ['Righteous'],
'lato': ['Lato'],
'raleway': ['Raleway'],
}
},
},
variants: {
extend: {},
},
plugins: [],
}
  • We’ve extended the components of the font by adding some fonts which we will use. These fonts have to be installed in your local machine to work appropriately but feel free to use whatever fonts you like.
  • Finally, create a index.css file in your src folder and add the following lines
/* ./src/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Installing Vue-fontawesome with Vue.js

npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/vue-fontawesome

or

yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/vue-fontawesome
yarn add @fortawesome/vue-fontawesome

Installing Vue-Axios

npm install --save axios vue-axios vue-router vuex

or

yarn add axios vue-axios vue-router vuex

Installing Vue-Progress-Path

npm i -S vue-progress-path
yarn add vue-progress-path

Integrating All the Installed Packages into Our Vue.js Application

  • Open up the main.js file that is located in the src folder and replace the contents of the file with the following code
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from 'axios'
import vueAxios from 'vue-axios'
import './index.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faArrowRight, faArrowLeft, faSearch, faBookmark, faShare, faClock, faCheck, faUserCircle, faTrash, faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import 'vue-progress-path/dist/vue-progress-path.css'
import VueProgress from 'vue-progress-path'

library.add(faArrowRight, faArrowLeft, faSearch, faBookmark, faShare, faClock, faCheck, faUserCircle, faTrash, faBars, faTimes)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.use(vueAxios, axios)
Vue.use(VueProgress, {
// defaultShape: 'circle',
})
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
  • Next, open the App.vue file in src and replace it with the following codes.
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
};
</script>

Building the Frontend

Building the Homepage

<template>
<div class="overflow-x-hidden">
<Nav class="z-20" />

<!-- Hero section -->
<HeroSection />
<!-- featured section -->
<FeaturedSection />
</div>
</template>
<script>
// @ is an alias to /src
import Nav from '@/components/Nav.vue'
import HeroSection from '@/components/HeroSection.vue'
import FeaturedSection from '@/components/FeaturedSection.vue'
export default {
name: 'Home',
components: {
Nav,
HeroSection,
FeaturedSection
}
}
</script>

Building our Nav Component

  • Execute the commands in other to create a Nav.vue file in the components folder
cd components
touch Nav.vue
  • Open up the Nav.vue file and fill it up with the following lines of code.
<template>
<div class="w-full bg-white fixed top-0 shadow-lg">

<div class="w-11/12 mx-auto flex justify-between justify-center items-center px-5 py-7">
<div class="text-black sm:text-left text-center text-4xl font-bold font-pacifico">
<h1>Recipee</h1>
</div>
<div @click="toggleMobileMenu" class="md:hidden">
<font-awesome-icon v-if='!mobileMenu' class="text-xl" :icon="['fas', 'bars']" />
<font-awesome-icon v-if='mobileMenu' class="text-xl" :icon="['fas', 'times']" />
</div>
<!-- desktop view -->
<div class="flex bg-white space-x-12 hidden sm:block text-black-200 font-raleway tracking-wide items-center">
<router-link to="/">HOME</router-link>
<router-link to="/explore">SEARCH RECIPES</router-link>
<router-link to="/register" v-if="!user">SIGN UP</router-link>
<router-link to="/login" v-if="!user">LOGIN</router-link>
<router-link to="/bookmarks" v-if="user">
<font-awesome-icon class="text-xl" :icon="['fas', 'bookmark']" /> BOOKMARKS
</router-link>
<router-link to="" v-if="user">
<font-awesome-icon class="text-xl" :icon="['fas', 'user-circle']" /> {{ user.username }}
</router-link>
<span @click="logout">
<router-link to="" v-if="user">LOGOUT</router-link>
</span>

</div>
</div>
<!-- mobile view -->
<div v-if="mobileMenu" class="h-screen md:hidden text-2xl text-left font-raleway p-10">
<router-link to="/" class="block my-7">HOME</router-link>
<hr>
<router-link to="/explore" class="block my-7">SEARCH RECIPES</router-link>
<hr>
<router-link to="/register" v-if="!user" class="block my-7">SIGN UP</router-link>
<hr>
<router-link to="/login" v-if="!user" class="block my-7">LOGIN</router-link>
<hr>
<router-link to="/bookmarks" v-if="user" class="block my-7">
<font-awesome-icon class="text-xl" :icon="['fas', 'bookmark']" /> BOOKMARKS
</router-link>
<hr>
<router-link to="" v-if="user" class="block my-7">
<font-awesome-icon class="text-xl" :icon="['fas', 'user-circle']" /> {{ user.username }}
</router-link>
<hr>
<span @click="logout" class="block my-7">
<router-link to="" v-if="user">LOGOUT</router-link>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'Nav',
data() {
return {
user: {},
mobileMenu: false
}
},
mounted() {
this.user = JSON.parse(window.localStorage.getItem('userData'))
},
methods: {
logout() {
window.localStorage.removeItem('jwt')
window.localStorage.removeItem('userData')
window.localStorage.removeItem('bookmarks')
this.$router.push('/login')
},
toggleMobileMenu() {
this.mobileMenu = !this.mobileMenu
}
}
}
</script>
<style scoped>
</style>

Building the HeroSection Component

  • Execute the commands in other to create a HeroSection.vue file in the components folder
cd components
touch HeroSection.vue
  • Open up the HeroSection.vue file and fill it up with the following lines of code.
<template>
<div>
<section>
<div class=" h-screen bg-cover" style="background: url(newFood.png)">
<div class="bg-blue-800 bg-opacity-50">
<div class="mx-auto h-screen flex text-white justify-left sm:w-4/5 items-center">
<div class="text-left font-montserrat mx-5 z-10">
<h1 class="text-6xl font-black my-10 z-10">
FIND THE
<br>
WORLD'S BEST
<br>
RECIPES ONLINE
</h1>
<router-link to='/explore' class="py-5 px-10 text-xl bg-green-600 z-10">
Search Recipes
<font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</router-link>
</div>
<div class="text-8xl absolute right-0 font-lato hidden sm:block font-bold overflow-y-hidden w-1/2">
<img src="../assets/undraw_breakfast-removebg-preview.png" alt="" class="w-full">
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'HeroSection'
}
</script>
<style scoped>
</style>

Building the FeaturedSection Component

  • Execute the commands in other to create a FeaturedSection.vue file in the components folder
cd components
touch FeaturedSection.vue
  • Open up the FeaturedSection.vue file and fill it up with the following lines of code.
<template>
<div>
<section>
<div class="relative">
<div class="sm:flex block mx-auto my-20 justify-center items-center">
<div class="z-10">
<div class="mx-auto mb-5 sm:mb-0 w-4/5 bg-pink-300 p-20">
<img src="../assets/burger.png" alt="" class="">
</div>
</div>

<div class="absolute top-0 right-0">
<img src="../assets/watercolor_stain.png" alt="" class="opacity-40 sm:opacity-70">
</div>
<div class="z-10">
<div class="mx-auto w-4/5 text-left font-raleway z-10">
<h1 class="font-bold text-black text-6xl mb-10">
THE BEST MEALS
<br>
IN THE UNIVERSE AWAITS
</h1>
<p class="text-sm tracking-wide font-montserrat mb-10">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
<br>
Facilis ex iure rem vero voluptate, sint praesentium quidem,
<br>
eius sequi, officia itaque? Eveniet quaerat eos qui sunt suscipit nisi sequi? Soluta.
</p>
<p class="text-xl text-black font-bold font-raleway">
EXPLORE
<font-awesome-icon class="" :icon="['fas', 'arrow-right']" />
</p>
</div>
</div>
</div>
</div>
<div class="relative">
<div class="block sm:flex my-20 justify-center items-center">
<div class="absolute top-0 left-0">
<img src="../assets/watercolor_drops.png" alt="" class="opacity-70">
</div>

<div class="z-10">
<div class="w-4/5 mx-auto text-left font-raleway z-10">
<h1 class="font-bold text-black text-6xl mb-10">
LEARN HOW
<br>
TO PREPARE MEALS YOU LOVE
</h1>
<p class="text-sm tracking-wide font-montserrat mb-10">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
<br>
Facilis ex iure rem vero voluptate, sint praesentium quidem,
<br>
eius sequi, officia itaque? Eveniet quaerat eos qui sunt suscipit nisi sequi? Soluta.
</p>
<p class="text-xl mb-5 sm:mb-0 text-black font-bold font-raleway">
EXPLORE
<font-awesome-icon class="" :icon="['fas', 'arrow-right']" />
</p>
</div>
</div>
<div class="">
<div class="mx-auto w-4/5 bg-green-300 p-20">
<img src="../assets/barbercue.png" alt="" class="">
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'FeaturedSection'
}
</script>
<style scoped>
</style>

Configuring Vue-Router

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Register from '../views/Register.vue'
import Login from '../views/Login.vue'
import Explore from '../views/Explore.vue'
import Recipe from '../views/Recipe.vue'
import Bookmarks from '../views/Bookmarks.vue'
import BookmarkId from '../views/BookmarkId.vue'
import ForgotPassword from '../views/ForgottenPassword.vue'
import ResetPassword from '../views/ResetPassword.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/explore',
name: 'Explore',
component: Explore
},
{
path: '/recipe/:id',
name: 'Recipe',
component: Recipe
},
{
path: '/bookmarks',
name: 'Bookmarks',
component: Bookmarks
},
{
path: '/bookmark/:id',
name: 'BookmarkId',
component: BookmarkId
},
{
path: '/forgotpassword',
name: 'ForgotPassword',
component: ForgotPassword
},
{
path: '/resetpassword',
name: 'ResetPassword',
component: ResetPassword
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

Configuring Vuex Store

  • Create a store folder in src folder, and create a new index.js file with the following code.
import Vue from "vue";
import Vuex from "vuex";
import Results from "./results.js";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
Results
}
});
  • Execute the following command to create a results.js file in the src/store directory
cd store
touch results.js
  • Open up the results.js file and fill it up with the following code.
import Vue from "vue";
const state = {
searchParam: '',
searchResults: [],
bookmarks: JSON.parse(window.localStorage.getItem('bookmarks'))
}
const getters = {
getSearchResults: state => state.searchResults,
getSearchParam: state => state.searchParam,
getBookmarks: state => {
return state.bookmarks
}
}
const actions = {
async fetchSearchResult ({ commit }, searchItem) {

const res = await Vue.axios.get(`https://api.edamam.com/search?q=${searchItem}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=20`)
const results = res.data.hits
commit('updateSearchResults', results)
},
async fetchSearchItem ({ commit }, item) {
commit('updateSearchItem', item)
}
}
const mutations = {
updateSearchResults: (state, results) => {
state.searchResults = results
},
updateSearchItem: (state, item) => {
state.searchParam = item
}
}
export default {
state,
getters,
actions,
mutations
}

Building the Explore Page

  • Execute the commands in other to create a Explore.vue file in the views folder
cd views
touch Explore.vue
  • Open up the Explore.vue file and fill it up with the following lines of code.
<template>
<div>
<Nav class="z-20" />
<section>
<div class="h-sreen w-full bg-cover" style="background: url(newFood.png)">
<div class="bg-blue-800 w-full bg-opacity-50">
<div class="mx-auto flex h-screen w-full justify-center items-center">
<div class="font-montserrat w-full text-white mx-5 z-10">
<h1 class="font-pacifico hidden sm:block text-6xl mb-10">Recipee</h1>
<!-- <h1 class="text-4xl mb-10 font-raleway">Search for whatsoever recipe you want</h1> -->
<form @submit="getRecipes">
<input type="text" name="search" v-model="search" placeholder="Search Recipe" class="p-10 focus:outline-none w-4/5 sm:w-3/5 text-black">
<button class="p-5 cursor-pointer bg-green-400">
<font-awesome-icon class="text-2xl" :icon="['fas', 'search']" />
</button>
</form>
</div>

<!-- <div v-if="loading" class="rounded-full absolute bottom-20 bg-blue-300 w-10 h-10">
</div> -->
<loading-progress class="absolute bottom-20" v-if="loading"
:progress="50"
:indeterminate='true'
:counter-clockwise="true"
:hide-background="false"
size="50"
rotate
fillDuration="2"
rotationDuration="1"
/>
</div>

</div>
</div>

</section>
<SearchResults />
</div>
</template>
<script>
import Nav from '@/components/Nav.vue'
import SearchResults from '@/components/SearchResults.vue'
import { mapActions } from 'vuex'
export default {
components: {
Nav,
SearchResults
},

data() {
return {
data : [],
search: '',
loading: false
}
},
methods: {
...mapActions(['fetchSearchResult']),
async getRecipes(e) {
this.loading = true
e.preventDefault()
this.fetchSearchResult(this.search).then(result => {
result;
this.loading = false
})
}
},
filters: {
capitalize(word) {
return word.toUpperCase()
}
},
async mounted() {}
}
</script>
<style scoped>
</style>

Building the SearchResults Component

  • Execute the commands in other to create a SearchResults.vue file in the components folder
cd components
touch SearchResults.vue
  • Open up the SearchResults.vue file and fill it up with the following lines of code.
<template>
<div>
<section>
<div v-if="getSearchResults.length > 1">
<h1 class="my-10 font-montserrat font-bold text-4xl">RESULTS</h1>
<div class="sm:grid sm:grid-cols-3 gap-5 w-4/5 sm:w-3/5 my-5 mx-auto">
<div class="mb-5 cursor-pointer" v-for="(item, i) in getSearchResults" :key="i">
<router-link :to='`/recipe/${item.recipe.label}`'>
<img :src='`${item.recipe.image}`' class="w-full" alt="">
</router-link>


<div class="p-5 shadow-lg">
<div class="flex space-x-4">
<button @click="addItemToBookmark(item.recipe)" class="click:text-yellow-400 rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl hover:text-yellow-400" :icon="['fas', 'bookmark']" />
</button>
<div class="rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl" :icon="['fas', 'share']" />
</div>
</div>
<router-link :to='`/recipe/${item.recipe.label}`'>
<h1 class="text-2xl font-bold font-montserrat mb-5">
{{ item.recipe.label }}
</h1>
</router-link>
<div class="text-md font-raleway tracking-wide">
<p>
{{ item.recipe.yield }} Servings | {{ item.recipe.ingredientLines.length }} Ingredients
</p>
<p v-if="item.recipe.totalTime > 0">
<font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ item.recipe.totalTime }} Minutes
</p>
</div>
</div>

</div>
</div>
</div>
</section>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'searchResult',
data() {
return {
bookmarks: JSON.parse(window.localStorage.getItem('bookmarks'))
}
},
methods: {
// ...mapActions(['addBookmark']),
async addItemToBookmark(item) {

if(window.localStorage.getItem('userData')) {
const { label, ingredientLines, totalTime, image, source, url } = item
let bookmarkItem
if(this.bookmarks.findIndex(recipe => recipe.label === item.label) === -1){
bookmarkItem = {
label,
ingredientLines,
totalTime,
image,
url,
source,
yield: item.yield,
users_permissions_user: JSON.parse(window.localStorage.getItem('userData')).id
}
this.bookmarks.push(bookmarkItem)
//set to localstorage
window.localStorage.setItem('bookmarks', JSON.stringify(this.bookmarks))
await this.axios.post(`http://localhost:1337/api/bookmarks`, {
data: {...bookmarkItem},
},
{
headers: {
Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
},
})
const res = await this.axios.get(`http://localhost:1337/api/users/${bookmarkItem.users_permissions_user}/?populate=*`, {
headers: {
Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
}
})
const user = res.data
window.localStorage.setItem('userData', JSON.stringify(user))
window.localStorage.setItem('bookmarks', JSON.stringify(user.bookmarks))
}
}
}

},
computed: {
...mapGetters(['getSearchResults', 'getBookmarks'])
}
}
</script>
<style scoped>
</style>

Building the Recipe Page

  • Execute the commands in other to create a Recipe.vue file in the views folder
cd views
touch Recipe.vue
  • Open up the Recipe.vue file and fill it up with the following lines of code.
<template>
<div>
<Nav class="relative" />
<div class="w-4/5 sm:w-3/5 mx-auto mt-10 text-left">
<div class="sm:grid grid-cols-2 gap-2">
<div>
<img class="mb-10" :src="`${curRecipe.image}`" alt="">
</div>
<h1 class="text-4xl sm:text-8xl font-bold font-montserrat">{{ name }}</h1>
</div>

<div class="text-xl mt-5 sm:mt-0 font-raleway tracking-wide flex space-x-5">
<p>
{{ curRecipe.yield }} Servings
</p>
<p> | </p>
<p v-if="curRecipe.totalTime > 0">
<font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ curRecipe.totalTime }} Minutes
</p>
</div>
<div class="mt-10">
<h1 class="text-2xl sm:text-4xl font-montserrat font-bold mb-10">
{{ curRecipe.ingredientLines.length }} Ingredients
</h1>
<div class="w-4/5 sm:grid font-raleway grid-cols-2 gap-2">
<div class="mb-5 mr-5" v-for="(Ingredients, i) in curRecipe.ingredientLines" :key="i">
<font-awesome-icon class="text-xl ml-3 text-green-300" :icon="['fas', 'check']" />
{{ Ingredients }}
</div>
</div>
</div>
<div class="mb-10 font-raleway">
<p class="mb-10"> Courtsey of <span class="text-2xl">{{ curRecipe.source }} </span></p>
<p >

<a class="py-5 px-10 text-xl bg-green-600 z-10 text-left text-white" target="blank" :href='`${curRecipe.url}`'>
Preparation Steps <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</a>
</p>
</div>
</div>

</div>
</template>
<script>
import Nav from '@/components/Nav.vue'
import { mapGetters } from 'vuex'
export default {
components: {
Nav
},
data() {
return {
name: this.$route.params.id,
curRecipe: {}
}
},
computed: {
...mapGetters(['getSearchResults'])
},
created() {
const recipeItem = this.getSearchResults.find(item => item.recipe.label === this.name)
this.curRecipe = recipeItem.recipe
}
}
</script>
<style scoped>
</style>

Building the Bookmarks Page

  • Execute the commands in other to create a Bookmarks.vue file in the views folder
cd views
touch Bookmarks.vue
  • Open up the Bookmarks.vue file and fill it up with the following lines of code.
<template>
<div>
<Nav />
<section>
<div v-if="bookmarks.length > 0">
<h1 class="mt-32 mb-4 font-montserrat font-bold text-4xl">Bookmarks</h1>
<div class="sm:grid sm:grid-cols-3 gap-5 w-4/5 sm:w-3/5 my-5 mx-auto">
<div class="mb-5 cursor-pointer" v-for="(item, i) in bookmarks" :key="i">
<router-link :to='`/bookmark/${item.label}`'>
<img :src='`${item.image}`' class="w-full" alt="">
</router-link>


<div class="p-5 shadow-lg">
<div class="flex space-x-4">
<button @click="removeItemFromBookmarks(item)" class="click:text-yellow-400 rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl hover:text-yellow-400" :icon="['fas', 'trash']" />
</button>
<div class="rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl" :icon="['fas', 'share']" />
</div>
</div>
<router-link :to='`/bookmark/${item.label}`'>
<h1 class="text-2xl font-bold font-montserrat mb-5">
{{ item.label }}
</h1>
</router-link>
<div class="text-md font-raleway tracking-wide">
<p>
{{ item.yield }} Servings | {{ item.ingredientLines.length }} Ingredients
</p>
<p v-if="item.totalTime > 0">
<font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ item.totalTime }} Minutes
</p>
</div>
</div>

</div>
</div>
</div>
</section>
</div>
</template>
<script>
// import { mapGetters } from 'vuex';
import Nav from '@/components/Nav.vue'
export default {
name: 'BookmarkPage',
components: {
Nav
},
data() {
return {
bookmarks: []
}
},

methods: {
async removeItemFromBookmarks(item) {
const itemIndex = this.bookmarks.findIndex(bookmarkItem => bookmarkItem.label === item.label)

this.bookmarks.splice(itemIndex, 1)
window.localStorage.setItem('bookmarks', JSON.stringify(this.bookmarks))
await this.axios.delete(`http://localhost:1337/api/bookmarks/${item.id}`, {
headers: {
Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
},
})
}
},
created() {
this.bookmarks = JSON.parse(window.localStorage.getItem('bookmarks'))
}
}
</script>
<style scoped>
</style>

Building the BookmarkID Page

  • Execute the commands in other to create a BookmarkId.vue file in the views folder
cd views
touch BookmarkId.vue
  • Open up the BookmarkId.vue file and fill it up with the following lines of code.
<template>
<div>
<Nav class="relative" />
<div class="w-4/5 sm:w-3/5 mx-auto mt-10 text-left">
<div class="sm:grid grid-cols-2 gap-2">
<div>
<img class="mb-10" :src="`${curRecipe.image}`" alt="">
</div>
<h1 class="text-4xl sm:text-8xl font-bold font-montserrat">{{ name }}</h1>
</div>

<div class="text-xl mt-5 sm:mt-0 font-raleway tracking-wide flex space-x-5">
<p>
{{ curRecipe.yield }} Servings
</p>
<p> | </p>
<p v-if="curRecipe.totalTime > 0">
<font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ curRecipe.totalTime }} Minutes
</p>
</div>
<div class="mt-10">
<h1 class="text-2xl sm:text-4xl font-montserrat font-bold mb-10">
{{ curRecipe.ingredientLines.length }} Ingredients
</h1>
<div class="w-4/5 sm:grid font-raleway grid-cols-2 gap-2">
<div class="mb-5 mr-5" v-for="(Ingredients, i) in curRecipe.ingredientLines" :key="i">
<font-awesome-icon class="text-xl ml-3 text-green-300" :icon="['fas', 'check']" />
{{ Ingredients }}
</div>
</div>
</div>
<div class="mb-10 font-raleway">
<p class="mb-10"> Courtsey of <span class="text-2xl">{{ curRecipe.source }} </span></p>
<p >
<a class="py-5 px-10 text-xl bg-green-600 z-10 text-left text-white" target="blank" :href='`${curRecipe.url}`'>
Preparation Steps <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</a>
</p>
</div>
</div>

</div>
</template>
<script>
import Nav from '@/components/Nav.vue'
export default {
name: 'BookmarkId',
components: {
Nav
},
data() {
return {
name: this.$route.params.id,
curRecipe: {},
bookmarkRecipes: JSON.parse(window.localStorage.getItem('bookmarks'))
}
},

created() {
const recipeItem = this.bookmarkRecipes.find(item => item.label === this.name)
this.curRecipe = recipeItem
}
}
</script>
<style scoped>
</style>

User Registration

Building the Registration Page

  • Execute the commands in other to create a Register.vue file in the views folder
cd views
touch Register.vue
  • Open up the Register.vue file and fill it up with the following lines of code.
<template>
<div>
<div class="flex items-center justify-center h-screen">
<div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
<div class="bg-blue-800 w-full h-screen bg-opacity-20">
</div>
</div>
<div class="sm:w-1/2">
<div class="p-5 w-4/5 mx-auto text-left font-raleway">
<div class="text-left mb-7">
<router-link to="/">
<font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> HOME
</router-link>
</div>
<h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-7">
Sign Up. To. Join Recipee
</h1>
<p v-show="error" class="text-sm text-red-500">{{ errorMsg }}</p>
<form @submit="register">
<div class="my-4">
<h1 class="text-left font-bold mb-2 font-montserrat">Name</h1>
<input type="text" v-model="name" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>
<div class="my-4">
<h1 class="text-left font-bold mb-2 font-montserrat">Email</h1>
<input type="email" v-model="email" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>
<div class="my-4">
<h1 class="text-left font-bold mb-2 font-montserrat">Password</h1>
<input type="password" v-model="password" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>
<div class="my-4">
<h1 class="text-left font-bold mb-2 font-montserrat">Username</h1>
<input type="text" v-model="username" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>

<button type="submit" :disabled="name.length < 6 || password.length < 6 || username.length < 3" class="bg-green-400 p-5 text-white">
Sign Up <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RegisterPage',
data() {
return {
name: '',
email: '',
password: '',
username: '',
error: false,
errorMsg: `An Error occurred, please try again`
}
},
methods: {
async register(e) {
try {
e.preventDefault()
await this.axios.post(`http://localhost:1337/api/auth/local/register`, {
name: this.name,
password: this.password,
email: this.email,
username: this.username
})
this.$router.push('login')
} catch(e) {
this.error = true
this.email = ''
}
}
}
}
</script>
<style scoped>
</style>

User Login

  • Execute the commands in other to create a Login.vue file in the views folder
cd views
touch Login.vue
  • Open up the Login.vue file and fill it up with the following lines of code.
<template>
<div>
<div class="flex items-center justify-center h-screen">
<div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
<div class="bg-blue-800 w-full h-screen bg-opacity-20">
</div>
</div>
<div class="sm:w-1/2">
<div class="p-5 w-4/5 mx-auto text-left font-raleway">
<div class="text-left mb-10">
<router-link to="/">
<font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> HOME
</router-link>
</div>

<h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
Login. To. Recipee
</h1>
<p v-show="error" class="text-sm text-red-500">{{ errorMsg }}</p>
<form @submit="login">
<div class="my-5">
<h1 class="text-left font-bold mb-5 font-montserrat">Email</h1>
<input type="email" v-model="email" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>
<div class="my-5">
<h1 class="text-left font-bold mb-5 font-montserrat">Password</h1>
<input type="password" v-model="password" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>

<button type="submit" :disabled="password.length < 3" class="bg-green-400 p-5 text-white">
Login <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</button>
<p class="my-2">
<router-link to="/forgotpassword" >Forgot Password?</router-link>
</p>

</form>
</div>
</div>
</div>
</div>
</template>
<script>

export default {
name: 'LoginPage',

data() {
return {
email: '',
password: '',
error: false,
errorMsg: `An error occurred, please try again`
}
},
methods: {
async login(e) {
e.preventDefault()

try {
const res = await this.axios.post(`http://localhost:1337/api/auth/local`, {
identifier: this.email,
password: this.password
});
const { jwt, user } = res.data
window.localStorage.setItem('jwt', jwt)
window.localStorage.setItem('userData', JSON.stringify(user))
const res2 = await this.axios.get(`http://localhost:1337/api/users/${user.id}?populate=*`, {
headers: {
Authorization: `Bearer ${jwt}`,
}
})
window.localStorage.setItem('bookmarks', JSON.stringify(res2?.data?.bookmarks || []))
this.$router.push('/')
} catch(error) {
this.error = true
this.password = ''
}
},
}
}
</script>
<style scoped>
</style>

Forgotten Password

  • Execute the commands in other to create a ForgottenPassword.vue file in the views folder
cd views
touch ForgottenPassword.vue
  • Open up the ForgottenPassword.vue file and fill it up with the following lines of code.
<template>
<div>
<div class="flex items-center justify-center h-screen">
<div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
<div class="bg-blue-800 w-full h-screen bg-opacity-20">
</div>
</div>
<div class="sm:w-1/2">
<div class="p-5 w-4/5 mx-auto text-left font-raleway">
<div class="text-left mb-10">
<router-link to="/login">
<font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> Login
</router-link>
</div>

<h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
Recover Your. Recipee. Password
</h1>
<p v-show="done" class="text-sm text-green-500">Password reset link has been sent to {{ email }}</p>
<p v-show="error" class="text-sm text-red-500">An error occurred</p>
<form @submit="forgotPassword">
<div class="my-5">
<h1 class="text-left font-bold mb-5 font-montserrat">Email</h1>
<input type="email" v-model="email" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>

<button type="submit" class="bg-green-400 p-5 text-white">
Send Email link <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ForgotPassword',

data() {
return {
email: '',
done: false,
error: false,
}
},
methods: {
async forgotPassword(e) {
e.preventDefault()
this.done = false;
this.error = false;
this.axios.post(`http://localhost:1337/api/auth/forgot-password`, {
email: this.email
})
.then(() => {
this.done = true
})
.catch(e => {
e;
this.error = true
})
}
}
}
</script>
<style scoped>
</style>

Setting up Email with Strapi-Provider-Email-Nodemailer

  • Open up your Strapi backend in your code editor and run the following command to install the Strapi-provider-email-nodemailer.
yarn add @strapi/provider-email-nodemailer
npm install @strapi/provider-email-nodemailer --save
  • Navigate to the config directory and create a plugins.js file and fill it up with the following code
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST', 'smtp.gmail.com'),
port: env('SMTP_PORT', 465),
auth: {
user: env('GMAIL_USER'),
pass: env('GMAIL_PASSWORD'),
},
// ... any custom nodemailer options
},
settings: {
defaultFrom: 'noreply@recipee.com',
defaultReplyTo: 'noreply@recipee.com',
},
},
},
});
  • Open up your .env file and add the following line of code
SMTP_HOST = smtp.gmail.com
SMTP_PORT = 465
GMAIL_USER = YOUR_GMAIL_ADDRESS
GMAIL_PASS = YOUR_GMAIL_PASSWORD

Reset Password

  • Execute the commands in other to create a ResetPassword.vue file in the views folder
cd views
touch ResetPassword.vue
  • Open up the ResetPassword.vue file and fill it up with the following lines of code.
<template>
<div>
<div class="flex items-center justify-center h-screen">
<div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
<div class="bg-blue-800 w-full h-screen bg-opacity-20">
</div>
</div>
<div class="sm:w-1/2">
<div class="p-5 w-4/5 mx-auto text-left font-raleway">
<div class="text-left mb-10">
<router-link to="/login">
<font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> Login
</router-link>
</div>

<h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
Recover Your. Recipee. Password
</h1>
<p v-show="error" class="text-sm text-red-500">An Error Occurred, Please Try Again</p>
<form @submit="resetPassword">
<div class="my-5">
<h1 class="text-left font-bold mb-5 font-montserrat">Password</h1>
<input type="password" v-model="password" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>
<div class="my-5">
<h1 class="text-left font-bold mb-5 font-montserrat">Confirm Password</h1>
<input type="password" v-model="confirmPassword" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
</div>

<button type="submit" :disabled="password.length < 3 || password !== confirmPassword" class="bg-green-400 p-5 text-white">
Reset Password <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>

export default {
name: 'ResetPassword',
data() {
return {
password: '',
confirmPassword: '',
done: false,
error: false,
}
},
methods: {
async resetPassword(e) {
e.preventDefault()
this.axios.post(`http://localhost:1337/api/auth/reset-password`, {
code: this.$route.query.code,
password: this.password,
passwordConfirmation: this.confirmPassword
})
.then(() => {
this.done = true
this.$router.push("login")
})
.catch(e => {
e;
this.error = true
})
}
},
}
</script>
<style scoped>
</style>

Conclusion

--

--

The open source Headless CMS Front-End Developers love.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store