How To Build a Productivity App Implementing Pomodoro With Nuxt.js and Auth0.

Build a productivity app implementing Pomodoro technique.

How To Build a Productivity App Implementing Pomodoro With Nuxt.js and Auth0.

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.

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 &copy {{date}}
    </div>
    <p class="text-lg py-4">Happy {{day}} &#128526;</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