How To Build a Productivity App Implementing Pomodoro With Nuxt.js and Auth0.
Build a productivity app implementing Pomodoro technique.
In this article, we are going to build a productivity application that implements the Pomodoro technique. We are going to build this application with
Vue.js
Nuxt.js
- Tailwind CSS (UI)
- Auth0
Prerequisites
For you to follow along easily, I wish you have a basic knowledge of JavaScript and Vue.js.
With this, coding in Nuxt.js will be a breeze for you.
We will also be implementing the use of log/logout into our application with Auth0.
Creating our Nuxt.js application.
To create s fresh nuxt.js application, you can use the following commands depending on your preferred package manager.
Also, remember to install Node.js if you haven't still you can download it here.
NPM
npm init nuxt-app <project-name>
NPX
npx create-nuxt-app <project-name>
YARN
yarn create nuxt-app <project-name>
After installing the nuxt.js application, you need to change your directory into the newly created Nuxt.js app and run the following command on your developer terminal.
npm run dev
That is if you are using NPM as your package manager. This command will serve our application into the development environment and make it available on localhost://3000.
Now we have our application running. It is now time to get our hands dirty with the code.
For the need of this application, we are going to scale it down to different components. The breakdown is as follow.
Components
Header component – Header.vue – Header section of our page
Pomodoro component – Pomofocus.vue – Timer section of the page
Usage component – Notes.vue – Section showing how to use the application.
Footer component – Footer.vue – Footer section of our application.
Separating different concerns of our application is essential in ensuring that we have less clutter in our application and makes it easier to read and make future updates.
On the application directory, navigate to the pages folder and open the index.vue file with your favourite code editor or IDE.
Creating relevant components.
Inside the components folder, create a file and give it the name Header.vue. this will be our application header section.
On the file, you need to update it with the following information.
<template>
<div class="home bg-purple-400 w-full">
<!-- start of the header component -->
<div class="pomofocus flex justify-around py-4 text-xl">
<div>
<H1>PomoFocus</H1>
</div>
<div>
<ul class="flex">
<li><nuxt-link to="/about" class="pr-4">About</nuxt-link></li>
<li>
<div v-if="!$auth.loading">
<div v-if="!$auth.isAuthenticated">
<button type="button" @click="logIn">Login</button>
</div>
<div v-if="$auth.isAuthenticated">
<Profile />
<button @click="logOut">LogOut</button>
</div>
</div>
</li>
</ul>
</div>
</div>
<!-- end of the header -->
<!--pomofocus component-->
<div class="text-center py-10">
<PomofocusTimer />
</div>
<div class="notes">
<Notes />
</div>
<Footer />
</div>
</template>
<script>
export default {
name: "Home",
components: {},
data: () => {
return {
isLogged: "",
};
},
methods: {
// Log the user in
logIn() {
this.$auth.loginWithRedirect();
},
// Log the user out
logOut() {
this.$auth.logout({
returnTo: window.location.origin,
});
},
},
};
</script>
<style scoped>
</style>
Creating the PomoFocus Component.
Here is where most of our application logic will go through. Here we will make a countdown timer of 25 minutes and set it to run on every second.
Create the file PomoFocus.vue file inside the components folder and update it as shown down below.
<template>
<!-- PomoFocus Timer Componenent -->
<div>
<div class="timer">
<div class="text-center flex justify-center pb-0 mb-0 mt-4 h-16">
<div v-for="tab in tabs" :key="tab">
<div class="tabs">
<button
class="
text-lg
ml-3
bg-purple-200
hover:bg-purple-300
rounded
py-1.5
px-5
"
>
{{ tab }}
</button>
</div>
</div>
</div>
<div>
<h4 class="text-9xl pt-0 mt-0 font-bold">
{{ timerMinutes }}:{{ timerSeconds }}
</h4>
<div class="button-toggle">
<button
class="
text-4xl
mt-4
rounded
font-bold
px-14
py-3
shadow-lg
bg-purple-600
hover:shadow-xl
hover:bg-purple-700
"
@click="start"
v-if="isActive === false"
>
START
</button>
<button
class="
text-4xl
mt-4
rounded
font-bold
px-14
py-3
shadow-lg
bg-purple-600
hover:shadow-xl
hover:bg-purple-700
"
@click="stop"
v-if="isActive === true"
>
STOP
</button>
</div>
</div>
</div>
<!-- TASKS SECTION -->
<div class="tasks">
<h2 class="pt-8 text-2xl py-6">Tasks:</h2>
<div class="w-1/4 my-0 mx-auto">
<hr class="py-4" />
</div>
<div v-for="task in tasks" :key="task.title" id="task.title">
<Task v-bind:task="task" v-on:deleteTask="deleteTask" />
</div>
<div class="addtask">
<form @submit.prevent="updateTasks">
<input
type="text"
class="
py-3
rounded
w-96
px-8
bg-purple-600
hover:bg-purple-800
text-white text-xl
mb-2
border-l-4
outline-none
text-center
"
name="task"
id="task"
v-model="addtask"
placeholder="Add tasks here ..."
/>
</form>
</div>
</div>
</div>
</template>
<script>
const notificationSound = require("@/assets/goeswithoutsaying.mp3").default;
export default {
name: "Home",
data() {
return {
isActive: false,
tasks: [
{
title: "edit youtube video",
edit: false,
complete: false,
},
{
title: "write a blog post",
edit: false,
complete: false,
},
],
tabs: ["Pomodoro", "Short Break"],
addtask: "",
timerType: 0,
totalSeconds: 25 * 60,
shortbreak: "5:00",
pomodoroInstance: null,
notificationSound,
};
},
computed: {
// show minutes
timerMinutes() {
const minutes = Math.floor(this.totalSeconds / 60);
return this.formatTime(minutes);
},
// show seconds
timerSeconds() {
let sec = this.totalSeconds % 60;
return this.formatTime(sec);
},
},
methods: {
// formats time function
formatTime(time) {
if (time < 10) {
return "0" + time;
}
return time.toString();
},
// start the timeer count
start() {
this.pomodoroInstance = setInterval(() => {
this.totalSeconds -= 1;
if (
Math.floor(this.totalSeconds / 60) === 0 &&
this.totalSeconds % 60 === 0
) {
var audio = new Audio(this.notificationSound);
audio.play();
clearInterval(this.pomodoroInstance);
(this.totalSeconds = 25 * 60), (this.isActive = false);
console.log(audio);
}
}, 1000);
this.isActive = true;
},
// stop the timer interval
stop() {
clearInterval(this.pomodoroInstance);
this.isActive = false;
},
// update the tasks
updateTasks() {
if (this.addtask === "") {
alert("Please enter task to proceed");
} else {
let newTask = {
title: this.addtask,
complete: false,
edit: false,
};
this.tasks.push(newTask);
this.addtask = "";
console.log("Task has been updated successfully");
}
},
// delete the task
deleteTask(id) {
this.tasks = this.tasks.filter((task) => task.title !== id);
},
},
};
</script>
<style scoped>
</style>
The code explanation is commented and you can follow along with minimal effort.
Remember to register the componenet in the index.vue file inside the pages folder or you can just render it inside the file as Nuxt automatically makes it a global component.
Creating the footer component
Inside the components folder, create a file and name it Footer.vue. This will be the footer section of our application.
Open the file and have the code shown below. It will be a simple footer showing footer section of the application and nothing much.
<template>
<div class="footer font-semibold text-center mt-20 text-xl leading-9 tracking-wide ">
<div class="coded text-xl"> </> with
<span><span class="sr-only">love</span>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="heart" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="animate-ping heart-background text-red-300 z-10 svg-inline--fa fa-heart fa-w-16">
<path fill="currentColor" d="M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z">
</path>
</svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="heart" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="text-red-600 svg-inline--fa fa-heart fa-w-16">
<path fill="currentColor" d="M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z">
</path>
</svg>
</span>by John Philip © {{date}}
</div>
<p class="text-lg py-4">Happy {{day}} 😎</p>
</div>
</template>
<script>
const date = new Date();
const day = date.toLocaleString("default", {
weekday: "long",
});
export default {
name: "Footer",
data() {
return {
date: new Date().getFullYear(),
day: day,
};
},
};
</script>
<style scoped>
.footer {
font-size: 1rem;
letter-spacing: 1px;
padding-bottom: 40px;
padding-top: 50px;
}
svg {
display: inline;
height: inherit;
width: 9px;
}
.svg-inline--fa,
svg:not(:root).svg-inline--fa {
overflow: visible;
}
.svg-inline--fa.fa-w-16 {
width: 1em;
}
.heart-background {
min-width: 1em;
min-height: 1em;
position: absolute;
margin-top: 4px;
opacity: 0.2;
}
.animate-ping {
-webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.z-10 {
z-index: 10;
}
.text-red-300 {
opacity: 1;
color: rgba(254, 178, 178, 1);
}
.svg-inline--fa {
display: inline-block;
font-size: inherit;
height: 1em;
vertical-align: -0.125em;
}
.svg-inline--fa {
display: inline-block;
font-size: inherit;
height: 1em;
vertical-align: -0.125em;
}
</style>
We have included some CSS classes to style the footer a bit.
Also remember to include the Footer component in the entry Index.vue file.
Create your application secret API keys
Navigate to the official Auth0 website and signup for an secret API tokens here.
You will also be needed to configure callback URLs and configure Logout URLs.
Registering the Auth0 plugin.
For Auth0 to function and for easy implementation in Nuxt.js, we need to register it as a plugin and it will be easily accessible anywhere in our Nuxt.js application instance.
Create a auth.js file
On the root of our application directory, create inside the src folder and name it auth. Inside the auth folder, create a file and give it the name auth.js.
This will be the file that will house all the entry points of our Auth0 log/logout functionality.
Inside the auth.js file, you need to have the code shown down below.
import Vue from "vue";
import createAuth0Client from "@auth0/auth0-spa-js";
/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
let instance;
/** Returns the current instance of the SDK */
export const getInstance = () => instance;
/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
redirectUri = window.location.origin,
...options
}) => {
if (instance) return instance;
// The 'instance' is simply a Vue object
instance = new Vue({
data() {
return {
loading: true,
isAuthenticated: false,
user: {},
auth0Client: null,
popupOpen: false,
error: null
};
},
methods: {
/** Authenticates the user using a popup window */
async loginWithPopup(options, config) {
this.popupOpen = true;
try {
await this.auth0Client.loginWithPopup(options, config);
this.user = await this.auth0Client.getUser();
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.error = null;
} catch (e) {
this.error = e;
// eslint-disable-next-line
console.error(e);
} finally {
this.popupOpen = false;
}
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
},
/** Handles the callback when logging in using a redirect */
async handleRedirectCallback() {
this.loading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
this.error = null;
} catch (e) {
this.error = e;
} finally {
this.loading = false;
}
},
/** Authenticates the user using the redirect method */
loginWithRedirect(o) {
return this.auth0Client.loginWithRedirect(o);
},
/** Returns all the claims present in the ID token */
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
},
/** Returns the access token. If the token is invalid or missing, a new one is retrieved */
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
/** Gets the access token using a popup window */
getTokenWithPopup(o) {
return this.auth0Client.getTokenWithPopup(o);
},
/** Logs the user out and removes their session on the authorization server */
logout(o) {
return this.auth0Client.logout(o);
}
},
/** Use this lifecycle method to instantiate the SDK client */
async created() {
// Create a new instance of the SDK client using members of the given options object
this.auth0Client = await createAuth0Client({
...options,
client_id: options.clientId,
redirect_uri: redirectUri
});
try {
// If the user is returning to the app after authentication..
if (
window.location.search.includes("code=") &&
window.location.search.includes("state=")
) {
// handle the redirect and retrieve tokens
const { appState } = await this.auth0Client.handleRedirectCallback();
this.error = null;
// Notify subscribers that the redirect callback has happened, passing the appState
// (useful for retrieving any pre-authentication state)
onRedirectCallback(appState);
}
} catch (e) {
this.error = e;
} finally {
// Initialize our internal authentication state
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.loading = false;
}
}
});
return instance;
};
// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
}
};
Now we need to create an instance of the auth0 functionality by registering the plugin. Create a folder on the root of the application and name it Plugins.
Inside the folder, create a file and give it the name auth.js. Inside the file, you need to have the code shown down below.
import Vue from "vue";
// Import the Auth0 configuration
import { domain, clientId } from "../auth_config.json";
// Import the plugin here
import { Auth0Plugin } from "../src/auth/index";
// Install the authentication plugin here
Vue.use(Auth0Plugin, {
domain,
clientId,
onRedirectCallback: appState => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname
);
}
});
Vue.config.productionTip = false;
Here we have create the plugin and now to make everything function, we need to navigate to the nuxt.config.js file and inside the plugins section, have the plugin registered as shown in the code snippet down below.
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
'~plugins/auth.js'
],
You can check out the application we have building through this link here here.
If you feel stack, you can feel free to check out the repository shown down below. or give your comments in the comments section