TypeScript Tutorial: Building a Simple Interactive Web-Based Pomodoro Timer

Are you struggling with procrastination and finding it hard to stay focused on your tasks? In today’s fast-paced world, time management is more crucial than ever. A Pomodoro Timer can be a game-changer, helping you break down work into focused intervals, interspersed with short breaks, to boost productivity and reduce mental fatigue. In this tutorial, we’ll dive into building a simple, yet effective, web-based Pomodoro Timer using TypeScript. This project is perfect for beginners and intermediate developers looking to enhance their skills with TypeScript and learn about event handling, DOM manipulation, and state management.

What is a Pomodoro Timer?

The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It involves working in focused 25-minute intervals, called “Pomodoros,” followed by a 5-minute break. After every four Pomodoros, you take a longer break, typically 20-30 minutes. This technique helps you stay focused, reduces distractions, and improves concentration.

Why TypeScript?

TypeScript, a superset of JavaScript, adds static typing to your JavaScript code. This means you can catch errors during development, making your code more robust and easier to maintain. Using TypeScript in this project will help you understand the benefits of static typing and how it can improve your development workflow. It also makes your code more readable, especially as projects grow in complexity.

Project Setup

Before we start, ensure you have Node.js and npm (Node Package Manager) installed on your system. You’ll also need a code editor like Visual Studio Code (VS Code) to write your code. Let’s create a new project and initialize it with npm:

mkdir pomodoro-timer
cd pomodoro-timer
npm init -y

Next, install TypeScript and a few other necessary packages:

npm install typescript --save-dev
npm install --save-dev @types/node @types/dom-parser

Now, let’s create a `tsconfig.json` file in your project root. This file tells the TypeScript compiler how to compile your code. You can generate a basic one using the following command:

npx tsc --init --rootDir src --outDir dist

This command creates a `tsconfig.json` file with default settings. We’ll modify a few settings to suit our project:

  • `rootDir`: Specifies the root directory of your input files.
  • `outDir`: Specifies the output directory for compiled JavaScript files.
  • `module`: Sets the module system (e.g., “esnext”, “commonjs”).
  • `target`: Sets the JavaScript language version for emitted JavaScript code (e.g., “es5”, “es2015”).

Here’s a sample `tsconfig.json` configuration:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Project Structure

Let’s create the following directory structure:

pomodoro-timer/
├── src/
│   ├── index.ts
│   ├── timer.ts
│   └── style.css
├── dist/
├── node_modules/
├── package.json
├── package-lock.json
└── tsconfig.json

The `src` directory will contain our TypeScript files, and the `dist` directory will hold the compiled JavaScript and CSS files.

Coding the Timer Logic (timer.ts)

Let’s start by creating the core timer logic in `src/timer.ts`. This file will handle the timer’s state, time calculations, and interval management.

// src/timer.ts

export enum TimerState {
    Stopped,
    Running,
    Paused,
    Break
}

export class Timer {
    private timeLeft: number;
    private intervalId: number | null = null;
    private timerState: TimerState = TimerState.Stopped;
    private readonly workDuration: number;
    private readonly breakDuration: number;
    private isWorkPeriod: boolean = true;
    private onTick: (time: number, state: TimerState) => void;
    private onComplete: () => void;

    constructor(
        workDuration: number = 25 * 60, // Default to 25 minutes
        breakDuration: number = 5 * 60, // Default to 5 minutes
        onTick: (time: number, state: TimerState) => void,
        onComplete: () => void
    ) {
        this.workDuration = workDuration;
        this.breakDuration = breakDuration;
        this.timeLeft = workDuration;
        this.onTick = onTick;
        this.onComplete = onComplete;
    }

    public start(): void {
        if (this.timerState === TimerState.Running) return; // Prevent starting if already running
        this.timerState = TimerState.Running;
        this.intervalId = window.setInterval(() => {
            this.tick();
        }, 1000);
    }

    public pause(): void {
        if (this.timerState !== TimerState.Running) return;
        this.timerState = TimerState.Paused;
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }

    public resume(): void {
        if (this.timerState !== TimerState.Paused) return;
        this.timerState = TimerState.Running;
        this.start();
    }

    public stop(): void {
        this.timerState = TimerState.Stopped;
        this.isWorkPeriod = true;
        this.timeLeft = this.workDuration;
        this.clearInterval();
        this.onTick(this.timeLeft, this.timerState);
    }

    private tick(): void {
        if (this.timerState !== TimerState.Running) return;

        this.timeLeft--;
        this.onTick(this.timeLeft, this.timerState);

        if (this.timeLeft <= 0) {
            this.onComplete();
            this.switchPeriod();
        }
    }

    private switchPeriod(): void {
        this.isWorkPeriod = !this.isWorkPeriod;
        this.timeLeft = this.isWorkPeriod ? this.workDuration : this.breakDuration;
        this.timerState = TimerState.Break;
        this.onTick(this.timeLeft, this.timerState);
        this.start();
    }

    private clearInterval(): void {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }

    public getTimeLeft(): number {
        return this.timeLeft;
    }

    public getState(): TimerState {
        return this.timerState;
    }
}

Let’s break down the code:

  • `TimerState` enum: Defines the possible states of the timer (Stopped, Running, Paused, Break).
  • `Timer` class: Encapsulates all the timer logic.
  • `constructor`: Initializes the timer with work and break durations. It also takes `onTick` and `onComplete` callbacks to update the UI and handle timer completion.
  • `start()`: Starts the timer by setting an interval that calls the `tick()` method every second.
  • `pause()`: Pauses the timer by clearing the interval.
  • `resume()`: Resumes the timer from a paused state.
  • `stop()`: Stops the timer and resets it to its initial state.
  • `tick()`: Decrements the `timeLeft` and calls the `onTick` callback. If the time reaches 0, it calls the `onComplete` callback and switches to the break or work period.
  • `switchPeriod()`: Switches between work and break periods.
  • `clearInterval()`: Helper function to clear the interval.
  • `getTimeLeft()`: Returns the remaining time.
  • `getState()`: Returns the current state of the timer.

Creating the User Interface (index.ts)

Now, let’s create the user interface in `src/index.ts`. This file will handle the HTML elements, event listeners, and the interaction with the `Timer` class.

// src/index.ts
import { Timer, TimerState } from './timer';
import './style.css';

const timeDisplay = document.getElementById('time') as HTMLSpanElement;
const startButton = document.getElementById('start') as HTMLButtonElement;
const pauseButton = document.getElementById('pause') as HTMLButtonElement;
const stopButton = document.getElementById('stop') as HTMLButtonElement;
const timerStateDisplay = document.getElementById('timerState') as HTMLSpanElement;

const workDurationInput = document.getElementById('workDuration') as HTMLInputElement;
const breakDurationInput = document.getElementById('breakDuration') as HTMLInputElement;
const applySettingsButton = document.getElementById('applySettings') as HTMLButtonElement;

let timer: Timer;
let workDuration: number = 25 * 60; // Default work duration (25 minutes)
let breakDuration: number = 5 * 60; // Default break duration (5 minutes)

function formatTime(seconds: number): string {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}

function updateDisplay(time: number, state: TimerState): void {
    timeDisplay.textContent = formatTime(time);
    timerStateDisplay.textContent = TimerState[state];
}

function setupTimer(): void {
    timer = new Timer(
        workDuration,
        breakDuration,
        (time, state) => {
            updateDisplay(time, state);
        },
        () => {
            // Optional: Add a sound notification here
            alert(timer.getState() === TimerState.Break ? 'Break time!' : 'Work time!');
        }
    );
    updateDisplay(timer.getTimeLeft(), timer.getState());
}

startButton.addEventListener('click', () => {
    timer.start();
});

pauseButton.addEventListener('click', () => {
    timer.pause();
});

stopButton.addEventListener('click', () => {
    timer.stop();
});

applySettingsButton.addEventListener('click', () => {
    workDuration = parseInt(workDurationInput.value, 10) * 60;
    breakDuration = parseInt(breakDurationInput.value, 10) * 60;
    if (timer) {
        timer.stop(); // Stop the current timer before applying new settings
    }
    setupTimer();
});


setupTimer();

Let’s break down the code:

  • Import necessary modules: Import the `Timer` class and the CSS file.
  • Get HTML elements: Select the HTML elements using their IDs to interact with the UI.
  • `formatTime()`: A helper function to format seconds into a mm:ss format.
  • `updateDisplay()`: Updates the time display on the UI.
  • `setupTimer()`: Instantiates a new `Timer` object and sets up the event listeners for the start, pause, and stop buttons.
  • Event Listeners: Add event listeners to the buttons to start, pause, and stop the timer.
  • Apply Settings Button: Adds an event listener to the apply settings button to update the work and break durations.
  • Initial Setup: Calls `setupTimer()` to initialize the timer.

Styling the Application (style.css)

Create a basic stylesheet in `src/style.css` to style the timer’s appearance. You can customize this to fit your preferences.

/* src/style.css */
body {
    font-family: sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    margin: 0;
    background-color: #f0f0f0;
}

#timer-container {
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    text-align: center;
}

#time {
    font-size: 3em;
    margin: 10px 0;
}

#timerState {
    font-size: 1.2em;
    margin-bottom: 20px;
}

button {
    padding: 10px 20px;
    font-size: 1em;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin: 5px;
}

button:hover {
    background-color: #3e8e41;
}

.settings-container {
    margin-top: 20px;
}

label {
    margin-right: 10px;
}

input[type="number"] {
    width: 60px;
    padding: 5px;
    border-radius: 4px;
    border: 1px solid #ccc;
}

Creating the HTML (index.html)

Create an `index.html` file in the root directory. This file will contain the HTML structure of the Pomodoro Timer.




    
    
    <title>Pomodoro Timer</title>


    <div id="timer-container">
        <div id="time">25:00</div>
        <div id="timerState">Stopped</div>
        <button id="start">Start</button>
        <button id="pause">Pause</button>
        <button id="stop">Stop</button>
    </div>
    <div class="settings-container">
        <label for="workDuration">Work Duration (minutes):</label>
        
        <label for="breakDuration">Break Duration (minutes):</label>
        
        <button id="applySettings">Apply Settings</button>
    </div>
    


This HTML provides the basic structure for the timer, including the time display, buttons for start, pause, and stop, and inputs for adjusting work and break durations. It also links to the compiled JavaScript file (`dist/index.js`).

Compiling and Running the Application

Now, compile your TypeScript code using the following command:

npx tsc

This command will compile your TypeScript files into JavaScript files in the `dist` directory. To run the application, open `index.html` in your web browser. You should see the Pomodoro Timer interface. You can now start, pause, and stop the timer.

Common Mistakes and Solutions

  • Incorrect File Paths: Ensure that your file paths in `index.ts` and `index.html` are correct. If the paths are incorrect, the application won’t load the necessary files. Double-check your import statements and the script tag in your HTML.
  • Typo Errors: Typos in variable names, function names, or HTML element IDs can cause errors. Carefully review your code for any typos.
  • Incorrect Event Listener Attachments: Make sure your event listeners are correctly attached to the HTML elements. Check that the element IDs match the ones used in your JavaScript code.
  • Incorrect TypeScript Configuration: If your TypeScript code is not compiling, check your `tsconfig.json` file. Ensure that the `rootDir` and `outDir` are configured correctly.
  • Uncaught Errors: Use your browser’s developer console to check for uncaught errors, which can help diagnose issues.

Key Takeaways

  • TypeScript for Type Safety: TypeScript enhances code quality and maintainability by catching errors early.
  • Component Design: The `Timer` class encapsulates the timer logic, promoting modularity and reusability.
  • Event Handling: Event listeners are used to handle user interactions.
  • DOM Manipulation: The application updates the UI by manipulating the DOM.
  • State Management: The `TimerState` enum and the `Timer` class manage the timer’s state.

FAQ

  1. Can I customize the work and break durations?
    Yes, the application allows you to change the work and break durations via input fields, and the timer will adapt to your settings when you click the “Apply Settings” button.
  2. How can I add a sound notification?
    You can add a sound notification within the `onComplete` callback in `src/index.ts`. Use the `new Audio()` constructor to play a sound file.
  3. Can I use this timer on my mobile device?
    Yes, the timer is web-based and should work on any device with a web browser. Ensure the viewport meta tag is set correctly for mobile responsiveness.
  4. How can I improve the UI?
    You can improve the UI by adding more CSS styling, using a CSS framework (e.g., Bootstrap, Tailwind CSS), and using more advanced UI elements.
  5. Can I add more features?
    Yes, you can add features such as saving session history, displaying a progress bar, or integrating with a task management system.

By following this tutorial, you’ve built a functional Pomodoro Timer using TypeScript. You’ve learned about the benefits of TypeScript, component design, event handling, and DOM manipulation. This project can be a starting point for more complex time management tools. The combination of focused work intervals and short breaks can dramatically improve productivity and reduce the stress of long work sessions. Now, you can use your new Pomodoro Timer to stay focused and make the most of your time. Experiment with different settings and find what works best for you!