Pomodoro timer

What pomodoro-timer project will be about

I stumbled upon pomodoro technique during my student times when I wanted to be more productive. It works great and I tried many different tools starting from web apps and ending on google play store. Recently I reread the pomodoro technique manifesto and I found out that I have missed one important aspect - tracking if 25min of work was without distraction. To accomplish that I started noting down which pomodoro was without distractions and which wasn't. I started noticing that I sometimes forgot to write down if pomodoro was good or not.

Then I had an idea - what if I write my own timer and at the end of 25min application will ask me: 'How productive last 25min was?'. Based on that I can start tracking my productivity throughout the day.

Moreover, I wanted to learn javascript so I decided to create my own pomodoro timer as a web page.

A few words about tools

In today javascript there are infinite number of tools, frameworks - by the way I recommend to read this piece.

I wanted to start from the basics without any framework to help me. I believe that frameworks come and go but understanding how language works stays. So I pick the newest javascript implementation -ECMAScript 6.

Then I started searching for web application template and I found one - Web Started Kit. I've opened it and looked inside the code. I looked one more time. So many tools! Sass, gulp, babel and other. I closed the editor. I removed this code and I started from scratch. I know it can help me a lot but I want to start from the basics. As I'm doing javascript course by Wes Bos I decided to use some tools that he is using. I really like browser-sync. It automatically reloads web pages when I change html, css or js files. To start browser-sync I have this one line in my package.json:

{
"scripts": {
"start": "browser-sync start --server --files '*.css, *.html, *.js'"
}
}

Then I write npm start.

When I learn a new language I always look for the best practices. In javascript word there is a couple of them but I choose Airbnb JavaScript Style Guide. Hot tool for linting js files right now is eslint. To use eslint with this style guide I installed eslint-config-airbnb. Thanks to that in my .eslintrc I wrote:

{
"extends": "airbnb"
}

Core functionality of pomodoro timer

As the name suggests the core functionality of a timer is to count down time. In the case of this timer, I will be using 25 minutes as a timer that needs to be counted down. I decided that for the time being, I will have only two control buttons for the timer: start & restart.

Implementing timer in JavaScript

As I know what I want to accomplish the first thing is the look of my timer. I was wondering if it will be better to write some CSS from scratch and learn this language too but when I start doing that I realized that I can spend a whole week only on this task. Instead, I decided to use Material Design Lite. This is a collection of CSS and JavaScript that allows me to use Google Material Design. To get started all I need to do is include some code from google CDN:

<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css" />
</head>
<body>
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</body>

You may have noticed that script has defer attribute which means that this script will be executed after the document has been parsed. I also add my custom style.css:

.display__time-left {
font-weight: 100;
font-size: 20rem;
margin: 0;
color: black;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.control_buttons {
flex: 1;
display: flex;
justify-content: space-around;
align-items: center;
}

Much of the code from style.css is based on Wes Bos code from here. In display__time-left I set up a few properties of the font that will be showing how many minutes and seconds are still in one pomodoro. I also made this element flex which fits element in its available space. .control_buttons are evenly spaced on the webpage with space between them by space-around. After loading a page it looks like this:

Basic layout
Basic layout

I am aware that this look needs a bit of work though. As I have my styles ready I add this HTML to the body:

<body>
<h1 class="display__time-left">25:00</h1>
<div class="control_buttons">
<button
class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent"
data-action="start"
>

Start
</button>

<button
class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent"
data-action="stop"
>

Restart
</button>
</div>

<audio id="end_sound" src="sound.wav"></audio>
</body>

At the beginning I show time left in pomodoro which by default is 25 minutes. Next, I have my control buttons with classes from Material Design Lite. At the end, there is an audio file which I will be playing at the end of each pomodoro.

How is the counting implemented? For this you need to look into script.js:

let countdown;
const timerDisplay = document.querySelector(".display__time-left");
const startTimeBtn = document.querySelector('[data-action="start"]');
const restartTimeBtn = document.querySelector('[data-action="stop"]');

Here I select necessary elements from HTML. I'm using querySelector to take class and data attributes. As I have my startTimeBtn selected then I add an event listener to it:

startTimeBtn.addEventListener("click", () => {
if (countdown) return;
timer(1500);
});

I'm listening for click event and if this happens I set up my timer for 1500 seconds which is 25 minutes. But before running timer(1500) I check if countdown element is defined. Why? Before the user can click as many times as he/ she wanted and start the timer from the beginning. Then I run timer:

function timer(seconds) {
const now = Date.now();
const then = now + seconds * 1000;
displayTimeLeft(seconds);
}

At the beginning, I define now which tells me what is current time right now. Then I foresee at which time my pomodoro timer will end. Then I call displayTimeLeft:

function displayTimeLeft(seconds) {
const minutes = Math.floor(seconds / 60);
const remainderSeconds = seconds % 60;
const display = `${minutes}:${remainderSeconds < 10 ? "0" : ""}${remainderSeconds}`;
timerDisplay.textContent = display;
}

Which is a simple function to display time in min:sec format. I compute minutes & remainderSeconds and then use es6 template string to neatly interpolate variables into the string. At the end, I set textContent of my timerDisplay which is h1 HTML element.

Let's go back to timer:

function timer(seconds) {
// variables

countdown = setInterval(() => {
const secondsLeft = Math.round((then - Date.now()) / 1000);

if (secondsLeft < 0) {
clearInterval(countdown);
playAudio();
return;
}

displayTimeLeft(secondsLeft);
}, 1000);

Here to countdown, I assign interval which will be executed every second. This is the place when this variable is defined and has an integer value. In the interval I calculate secondsLeft and if they are less than 0 it means it's time to stop interval by clearInterval, play sound and exit the function. At the end, I display changing time. playAudio is a simple function:

const endSound = document.querySelector("#end_sound");

function playAudio() {
const sound = new Audio(endSound.src);
sound.play();
}

By the way most of these functions I take from JavaScript 30 day 29 by Wes Bos.

There is the last thing to do - restart my timer:

restartTimeBtn.addEventListener("click", () => {
clearInterval(countdown);
countdown = undefined;
timerDisplay.textContent = "25:00";
});

I stop interval, set the countdown to undefined so I can start my timer again. I also redisplay remaining time.

What is next?

That's all for today! Thanks for reading but don't worry there is still a lot to do:

Notifications in JavaScript

I want my pomodoro timer to run in the background - I decided my website will be one of many pinned tabs in my Chrome. But this statement makes some complications - how do I know that my pomodoro ends? How do I tick if this 25 minutes was good or bad? I need a way to tell a user that it is time for a break. For this task, I will use notifications.

JavaScript notification standard says that:

A notification is an abstract representation of something that happened, such as the delivery of a message.

Cool! Exactly what I need - let's jump into the code.

How I implemented notifications

At first, you have to ask a user for permission to display notifications:

let notificationPermission = false;

Notification.requestPermission().then((result) => {
if (result === "granted") {
notificationPermission = true;
}
});

At the beginning I setup variable that will be a flag for my code to know if user grants permission to display notifications. This is done below. Notification.requestPermission() returns promise so I call then and if result is granted I set a flag variable to be true.

I want my notifications to be displayed when my time ends:

function displayNotification() {
if (!notificationPermission) return;
const notification = new Notification("Time's up!", {
icon: "stopwatch.png",
});

notification.addEventListener("click", () => {
window.focus();
});
}

function timer(seconds) {
// rest of the code

if (secondsLeft < 0) {
// rest of the if
displayNotification();
}
}

In displayNotification I check if user granted permission to see notification, if not the function terminates. Then I create new notification with stopwatch icon and Time's up! text. notification can have event listener attached to click event so I did that so when user clicks notification it will switch him/ her to pomodoro timer tab.

This is how my notification looks like:

Notification
Notification

Breaks for pomodoro timer

The whole idea of pomodoro is that after every 25 minutes of work you have 5 minutes of break to get up from computer and rest. I wanted to implement the same in my timer.

As I already have function timer to which I pass number of seconds to countdown, all I need to do is to figure out when to call timer(300) for 5 minutes break.

I did this by passing flag to timer called hasBreakAfter - if timer has break after call timer with 300 seconds if not go on with the flow.

function timer(seconds, hasBreakAfter = true) {
// function body
if (secondsLeft < 0) {
makeBreak(hasBreakAfter);
}
}

function makeBreak(hasBreak) {
if (hasBreak) {
timer(300, false);
}
}

I also have to adjust notifications text:

// inside if in timer function
displayNotification(hasBreakAfter ? "Time to rest dude!" : "Time to work dude!");

Displaying & storing breaks and pomodoros

I have my breaks and pomodoros but it will be awesome to present it to the user so he can see how much time he works today.

After notification is displayed I use saveTimeEntryToLocalStorage:

const entries = JSON.parse(localStorage.getItem("entries")) || [];

function extractHoursMinutes(date) {
return date.split(" ").splice(4, 1)[0].slice(0, 5);
}

function saveTimeEntryToLocalStorage(startSeconds, endSeconds, type) {
const startTime = extractHoursMinutes(Date(startSeconds));
const endTime = extractHoursMinutes(Date(endSeconds));

const entry = {
startTime,
endTime,
type,
};
entries.push(entry);
localStorage.setItem("entries", JSON.stringify(entries));
}

To this function I pass: when given entry starts, end and what type it was. First thing is to extract hours:minutes from JavaScript Date. I do this in extractHoursMinutes by some play with arrays and strings.

JavaScript Date returns full date:"Sun Mar 05 2017 11:59:19 GMT+0100 (CET)". I split this string by whitespace then I take the fourth element which is hours:minutes:seconds and returns only hours:minutes.

I have my start and end time ready I can create entry object which then I add to entries. entries are JavaScript list of objects from localStorage. If nothing is currently in localStorage list is empty. At the end I store updated entries in localStorage by localStorage.setItem().

The last thing is to get these entries and render them to the end user:

function retrieveTimeEntryFromLocalStorage() {
tableBody.innerHTML = entries
.map(
(entry) => `
<tr>
<td class="mdl-data-table__cell--non-numeric">
${entry.startTime}</td>
<td class="mdl-data-table__cell--non-numeric">
${entry.endTime}</td>
<td class="mdl-data-table__cell--non-numeric">
${entry.type}</td>
</tr>
`

)
.join("");
}

Here is iterate over entries and take every one of them add to respective td and save to HTML. Be sure that you spell HTML in innerHTML capitalised as I was debugging this error for quite a time.

One more thing is to reset and clear localStorage:

resetLocalStorageBtn.addEventListener("click", () => {
localStorage.clear();
window.location.reload(true);
});

I also reload page without cache by window.location.reload(true).

Entries look as follows:

Storage
Storage

How to check how good your pomodoro was?

When did I decide that I want my pomodoro timer to record if my 25 minutes work was worth something I have this burning question: How to do it? Some time ago I used a tool called Kanbanflow. It has great pomodoro extension where you can select if your pomodoro was good. Based on that I started thinking what if at the end of 25 minutes I display modal to the end user: please select how good was your pomodoro?. Which this thought in my head I start coding.

Modals in javascript

First I need some HTML structure for my modal:

<div class="is-hidden modal-overlay">
<div class="modal">
<h2 class="modal_question">How was your pomodoro?</h2>
<div class="modal_buttons">
<button
class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"
data-productive="true"
>

Productive
</button>
<button
class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent"
data-productive="false"
>

Not really productive
</button>
</div>
</div>
</div>

I apply some styling - thanks to that when modal is displayed rest of the web page is dimmed:

.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}

.modal {
padding: 20px 30px;
width: 90%;
max-height: calc(100% - 150px);
position: relative;
min-height: 300px;
margin: 5% auto 0;
background: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

I apply some styling to buttons and questions too:

.is-hidden {
display: none;
}

.modal .modal_question {
flex: 2;
}

.modal_buttons {
display: flex;
justify-content: space-between;
flex-direction: row;
flex: 1;
}

.modal_buttons .mdl-button {
margin-right: 10px;
}

Thanks to that my modal looks like this:

Modal for pomodoro timer
Modal for pomodoro timer

As I have my CSS & HTML done right now it's time to write some code in javascript.

Firstly, I have to add a new argument for entry in localStorage - wasGood. It is boolean so I know if this time entry was good or not:

function saveTimeEntryToLocalStorage(startSeconds, endSeconds, type, wasGood) {
// rest of the function body
const entry = {
startTime,
endTime,
type,
wasGood,
};
entries.push(entry);
localStorage.setItem("entries", JSON.stringify(entries));
}

As the saving of entry cannot be invoked when timer stops - as the user have to click the button first with productive or not productive pomodoro I have to introduce two global variables so I can access them not only from timer function:

const modal = document.querySelector(".modal-overlay");
const modalButtons = modal.querySelectorAll("[data-productive]");
let now;
let then;

function timer(seconds, hasBreakAfter = true) {
now = Date.now();
then = now + seconds * 1000;
// rest of timer body
}

modalButtons.forEach((button) => {
button.addEventListener("click", closeModal);
});

The last 3 lines of code are setting up the event listeners for both of buttons in the modal. When a user clicks one of them I run closeModal:

function closeModal(event) {
modal.classList.add("is-hidden");
saveTimeEntryToLocalStorage(now, then, "Pomodoro", event.target.dataset.productive);
retrieveTimeEntryFromLocalStorage();
}

It closes modal by adding is-hidden which is equivalent to display: none. Then I save entry and retrieve it. As I wanted something different than true or false to be displayed to end user I have updated retriveTimeEntryFromLocalStorage:

function retrieveTimeEntryFromLocalStorage() {
tableBody.innerHTML = entries
.map(
(entry) => `
<tr>
<td class="mdl-data-table__cell--non-numeric">
${entry.wasGood === true ? "✔" : "✖"}</td>
</tr>
`

)
.join("");
}

The last thing I have to do was to display modal when pomodoro ends:

function timer(seconds, hasBreakAfter = true) {
// function body

if (secondsLeft < 0) {
// do the break, display notfication, play sound
if (hasBreakAfter) modal.classList.remove("is-hidden");
}
}

And timer works! If you want to see it in action go here. That's all for today blog post - stay tuned for the next. Feel free to comment!

Repo with this code is available on github.

References