Nguyen Tien Phat

I'm currently a senior student at Sai Gon University

Ho Chi Minh City, Vietnam
English & Vietnam
Total time coded since Apr 15 2023
6 stars earned on Github
1863 words
10 min read

Pomodoro

A simple pomodoro timer with many cool features

2023-08-06
React / TailwindCSS

Building a Feature-Rich Pomodoro Timer with React and TailwindCSS

The Pomodoro Technique is a time management method that uses a timer to break work into intervals, traditionally 25 minutes in length, separated by short breaks. As a developer who struggles with focus and productivity, I decided to build my own Pomodoro timer with modern web technologies and additional features that enhance the traditional technique.

Project Overview ๐Ÿ…

My Pomodoro timer isnโ€™t just another basic countdown timer. Itโ€™s a comprehensive productivity tool built with React and TailwindCSS that includes multiple features to help users stay focused and track their productivity over time.

Key Features

  • โฑ๏ธ Customizable timer intervals (work, short break, long break)
  • ๐ŸŽต Multiple notification sounds with volume control
  • ๐Ÿ“Š Session tracking and statistics
  • ๐ŸŽจ Beautiful, responsive UI with dark/light themes
  • ๐Ÿ“ฑ Mobile-friendly design
  • ๐Ÿ’พ Local storage for settings persistence
  • ๐Ÿ”” Browser notifications support
  • ๐Ÿ“ˆ Progress visualization

Technical Stack ๐Ÿ› ๏ธ

Core Technologies

  • React 18: For building the user interface with hooks and functional components
  • TailwindCSS: For rapid styling and responsive design
  • JavaScript (ES6+): Modern JavaScript features and APIs
  • Local Storage API: For persisting user settings and session data
  • Web Audio API: For custom notification sounds
  • Notification API: For browser notifications

Development Tools

  • Vite: Lightning-fast build tool and development server
  • ESLint: Code linting and formatting
  • PostCSS: CSS processing and optimization

Architecture and Design Decisions ๐Ÿ—๏ธ

Component Structure

src/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ Timer/
โ”‚   โ”‚   โ”œโ”€โ”€ TimerDisplay.jsx
โ”‚   โ”‚   โ”œโ”€โ”€ TimerControls.jsx
โ”‚   โ”‚   โ””โ”€โ”€ ProgressRing.jsx
โ”‚   โ”œโ”€โ”€ Settings/
โ”‚   โ”‚   โ”œโ”€โ”€ SettingsModal.jsx
โ”‚   โ”‚   โ”œโ”€โ”€ SoundSelector.jsx
โ”‚   โ”‚   โ””โ”€โ”€ ThemeToggle.jsx
โ”‚   โ”œโ”€โ”€ Statistics/
โ”‚   โ”‚   โ”œโ”€โ”€ SessionStats.jsx
โ”‚   โ”‚   โ””โ”€โ”€ ProgressChart.jsx
โ”‚   โ””โ”€โ”€ UI/
โ”‚       โ”œโ”€โ”€ Button.jsx
โ”‚       โ”œโ”€โ”€ Modal.jsx
โ”‚       โ””โ”€โ”€ Notification.jsx
โ”œโ”€โ”€ hooks/
โ”‚   โ”œโ”€โ”€ useTimer.js
โ”‚   โ”œโ”€โ”€ useSettings.js
โ”‚   โ””โ”€โ”€ useNotifications.js
โ”œโ”€โ”€ utils/
โ”‚   โ”œโ”€โ”€ timeFormat.js
โ”‚   โ”œโ”€โ”€ storage.js
โ”‚   โ””โ”€โ”€ audio.js
โ””โ”€โ”€ styles/
    โ””โ”€โ”€ globals.css

Custom Hooks Implementation

useTimer Hook

The heart of the application is the useTimer hook that manages all timer-related state and logic:

import { useState, useEffect, useRef } from 'react';
 
const useTimer = (initialTime = 25 * 60) => {
  const [time, setTime] = useState(initialTime);
  const [isRunning, setIsRunning] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [sessionType, setSessionType] = useState('work');
  const [sessionCount, setSessionCount] = useState(0);
  const intervalRef = useRef(null);
 
  useEffect(() => {
    if (isRunning && time > 0) {
      intervalRef.current = setInterval(() => {
        setTime(prevTime => prevTime - 1);
      }, 1000);
    } else if (time === 0) {
      handleSessionComplete();
    }
 
    return () => clearInterval(intervalRef.current);
  }, [isRunning, time]);
 
  const handleSessionComplete = () => {
    setIsRunning(false);
    // Play notification sound
    // Show browser notification
    // Switch to next session type
    switchSessionType();
  };
 
  const switchSessionType = () => {
    if (sessionType === 'work') {
      setSessionCount(prev => prev + 1);
      // After 4 work sessions, take a long break
      const nextType = sessionCount % 4 === 3 ? 'longBreak' : 'shortBreak';
      setSessionType(nextType);
    } else {
      setSessionType('work');
    }
  };
 
  const startTimer = () => {
    setIsRunning(true);
    setIsPaused(false);
  };
 
  const pauseTimer = () => {
    setIsRunning(false);
    setIsPaused(true);
  };
 
  const resetTimer = () => {
    setIsRunning(false);
    setIsPaused(false);
    setTime(initialTime);
  };
 
  return {
    time,
    isRunning,
    isPaused,
    sessionType,
    sessionCount,
    startTimer,
    pauseTimer,
    resetTimer,
    setTime
  };
};

useSettings Hook

Managing user preferences and settings:

import { useState, useEffect } from 'react';
 
const useSettings = () => {
  const [settings, setSettings] = useState({
    workDuration: 25,
    shortBreakDuration: 5,
    longBreakDuration: 15,
    volume: 0.5,
    selectedSound: 'bell',
    theme: 'light',
    autoStartBreaks: false,
    autoStartWork: false,
    notifications: true
  });
 
  useEffect(() => {
    // Load settings from localStorage
    const savedSettings = localStorage.getItem('pomodoroSettings');
    if (savedSettings) {
      setSettings(JSON.parse(savedSettings));
    }
  }, []);
 
  useEffect(() => {
    // Save settings to localStorage whenever they change
    localStorage.setItem('pomodoroSettings', JSON.stringify(settings));
  }, [settings]);
 
  const updateSetting = (key, value) => {
    setSettings(prev => ({
      ...prev,
      [key]: value
    }));
  };
 
  return { settings, updateSetting };
};

Key Features Implementation ๐Ÿš€

1. Customizable Timer Intervals

Users can adjust work and break durations according to their preferences:

const SettingsModal = ({ settings, updateSetting, isOpen, onClose }) => {
  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
      <div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
        <h2 className="text-xl font-bold mb-4">Settings</h2>
        
        <div className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">
              Work Duration (minutes)
            </label>
            <input
              type="range"
              min="1"
              max="60"
              value={settings.workDuration}
              onChange={(e) => updateSetting('workDuration', parseInt(e.target.value))}
              className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
            />
            <span className="text-sm text-gray-600">{settings.workDuration} minutes</span>
          </div>
          
          {/* Similar inputs for short break and long break */}
        </div>
      </div>
    </div>
  );
};

2. Progress Visualization

A circular progress ring that shows the current session progress:

const ProgressRing = ({ progress, sessionType }) => {
  const radius = 90;
  const circumference = 2 * Math.PI * radius;
  const strokeDashoffset = circumference - (progress / 100) * circumference;
 
  const getColorBySession = (type) => {
    switch (type) {
      case 'work': return '#ef4444'; // Red for work
      case 'shortBreak': return '#10b981'; // Green for short break
      case 'longBreak': return '#3b82f6'; // Blue for long break
      default: return '#6b7280';
    }
  };
 
  return (
    <svg className="w-48 h-48 transform -rotate-90" viewBox="0 0 200 200">
      {/* Background circle */}
      <circle
        cx="100"
        cy="100"
        r={radius}
        stroke="currentColor"
        strokeWidth="8"
        fill="none"
        className="text-gray-200 dark:text-gray-700"
      />
      
      {/* Progress circle */}
      <circle
        cx="100"
        cy="100"
        r={radius}
        stroke={getColorBySession(sessionType)}
        strokeWidth="8"
        fill="none"
        strokeDasharray={circumference}
        strokeDashoffset={strokeDashoffset}
        strokeLinecap="round"
        className="transition-all duration-1000 ease-in-out"
      />
    </svg>
  );
};

3. Session Statistics

Tracking productivity metrics and displaying them to users:

const useStatistics = () => {
  const [stats, setStats] = useState({
    totalSessions: 0,
    totalWorkTime: 0,
    totalBreakTime: 0,
    sessionsToday: 0,
    currentStreak: 0,
    longestStreak: 0,
    dailyGoal: 8
  });
 
  const updateStats = (sessionType, duration) => {
    setStats(prev => {
      const newStats = { ...prev };
      
      if (sessionType === 'work') {
        newStats.totalSessions += 1;
        newStats.totalWorkTime += duration;
        newStats.sessionsToday += 1;
        newStats.currentStreak += 1;
        newStats.longestStreak = Math.max(newStats.longestStreak, newStats.currentStreak);
      } else {
        newStats.totalBreakTime += duration;
      }
      
      return newStats;
    });
  };
 
  return { stats, updateStats };
};

4. Notification System

Browser notifications and audio alerts:

const useNotifications = () => {
  const [permission, setPermission] = useState(Notification.permission);
 
  const requestPermission = async () => {
    const result = await Notification.requestPermission();
    setPermission(result);
  };
 
  const showNotification = (title, body) => {
    if (permission === 'granted') {
      new Notification(title, {
        body,
        icon: '/pomodoro-icon.png',
        badge: '/pomodoro-badge.png'
      });
    }
  };
 
  const playSound = (soundName, volume = 0.5) => {
    const audio = new Audio(`/sounds/${soundName}.mp3`);
    audio.volume = volume;
    audio.play().catch(console.error);
  };
 
  return { permission, requestPermission, showNotification, playSound };
};

Responsive Design with TailwindCSS ๐Ÿ“ฑ

The application is fully responsive and works seamlessly across all devices:

const TimerDisplay = ({ time, sessionType, progress }) => {
  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  };
 
  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-4">
      <div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl p-8 md:p-12 max-w-md w-full">
        {/* Session type indicator */}
        <div className="text-center mb-6">
          <span className="inline-block px-4 py-2 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm font-medium uppercase tracking-wider">
            {sessionType === 'work' ? 'Work Time' : 
             sessionType === 'shortBreak' ? 'Short Break' : 'Long Break'}
          </span>
        </div>
 
        {/* Timer display */}
        <div className="relative flex items-center justify-center mb-8">
          <ProgressRing progress={progress} sessionType={sessionType} />
          <div className="absolute inset-0 flex items-center justify-center">
            <span className="text-4xl md:text-5xl font-mono font-bold text-gray-800 dark:text-white">
              {formatTime(time)}
            </span>
          </div>
        </div>
 
        {/* Timer controls */}
        <TimerControls />
      </div>
    </div>
  );
};

Performance Optimizations โšก

1. Efficient Re-renders

Using React.memo and useMemo for expensive calculations:

const TimerDisplay = React.memo(({ time, sessionType, progress }) => {
  const formattedTime = useMemo(() => {
    const mins = Math.floor(time / 60);
    const secs = time % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }, [time]);
 
  return (
    <div className="text-4xl font-mono font-bold">
      {formattedTime}
    </div>
  );
});

2. Local Storage Optimization

Debouncing storage operations to prevent excessive writes:

const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading from localStorage:', error);
      return initialValue;
    }
  });
 
  const setValue = useCallback(
    debounce((value) => {
      try {
        setStoredValue(value);
        localStorage.setItem(key, JSON.stringify(value));
      } catch (error) {
        console.error('Error writing to localStorage:', error);
      }
    }, 300),
    [key]
  );
 
  return [storedValue, setValue];
};

Challenges and Solutions ๐Ÿ’ก

Challenge 1: Timer Accuracy

Problem: JavaScript timers can be inaccurate due to browser throttling
Solution: Implemented a time-based approach that calculates elapsed time

const useAccurateTimer = (duration) => {
  const [timeLeft, setTimeLeft] = useState(duration);
  const startTimeRef = useRef(null);
 
  useEffect(() => {
    if (isRunning) {
      startTimeRef.current = Date.now();
      const interval = setInterval(() => {
        const elapsed = Date.now() - startTimeRef.current;
        const remaining = Math.max(0, duration - Math.floor(elapsed / 1000));
        setTimeLeft(remaining);
      }, 100);
 
      return () => clearInterval(interval);
    }
  }, [isRunning, duration]);
 
  return timeLeft;
};

Challenge 2: Background Tab Behavior

Problem: Timers pause when the tab is in the background
Solution: Used Web Workers for background timer execution

// worker.js
let timerId;
let startTime;
let duration;
 
self.onmessage = function(e) {
  const { type, payload } = e.data;
  
  switch (type) {
    case 'START':
      startTime = Date.now();
      duration = payload.duration;
      timerId = setInterval(() => {
        const elapsed = Date.now() - startTime;
        const remaining = Math.max(0, duration - Math.floor(elapsed / 1000));
        self.postMessage({ type: 'TICK', payload: { timeLeft: remaining } });
        
        if (remaining === 0) {
          clearInterval(timerId);
          self.postMessage({ type: 'COMPLETE' });
        }
      }, 1000);
      break;
      
    case 'PAUSE':
      clearInterval(timerId);
      break;
      
    case 'STOP':
      clearInterval(timerId);
      break;
  }
};

Challenge 3: Sound Management

Problem: Multiple sound instances playing simultaneously
Solution: Created a sound manager with proper cleanup

class SoundManager {
  constructor() {
    this.currentAudio = null;
    this.sounds = new Map();
  }
 
  preloadSounds() {
    const soundFiles = ['bell', 'chime', 'ding', 'notification'];
    soundFiles.forEach(sound => {
      const audio = new Audio(`/sounds/${sound}.mp3`);
      this.sounds.set(sound, audio);
    });
  }
 
  play(soundName, volume = 0.5) {
    this.stop(); // Stop any currently playing sound
    
    const audio = this.sounds.get(soundName);
    if (audio) {
      audio.volume = volume;
      audio.currentTime = 0;
      this.currentAudio = audio;
      return audio.play();
    }
  }
 
  stop() {
    if (this.currentAudio) {
      this.currentAudio.pause();
      this.currentAudio.currentTime = 0;
      this.currentAudio = null;
    }
  }
}

Future Enhancements ๐Ÿ”ฎ

Planned Features

  • Cloud sync for cross-device statistics
  • Team collaboration features
  • Integration with calendar apps
  • Advanced analytics and insights
  • Customizable themes and animations
  • Spotify integration for focus music
  • Goal setting and achievement system

Technical Improvements

  • PWA support for offline functionality
  • Push notifications for mobile devices
  • Accessibility enhancements (ARIA labels, keyboard navigation)
  • Performance monitoring and optimization
  • Error boundary implementation
  • Unit and integration tests

Lessons Learned ๐Ÿ“š

Technical Learnings

  1. Timer precision: Browser timers have limitations that require creative solutions
  2. State management: Complex state requires careful architecture planning
  3. Performance: React optimization techniques are crucial for smooth UX
  4. Web APIs: Browser APIs like Notification and Audio require proper handling

Design Learnings

  1. User experience: Simple, intuitive interfaces are more effective
  2. Accessibility: Designing for all users improves the overall experience
  3. Responsive design: Mobile-first approach ensures broad compatibility
  4. Visual feedback: Progress indicators and animations enhance user engagement

Conclusion ๐ŸŽฏ

Building this Pomodoro timer was an excellent learning experience that combined practical productivity needs with modern web development techniques. The project demonstrates:

  • React best practices with hooks and functional components
  • Responsive design with TailwindCSS
  • Browser API integration for notifications and audio
  • Performance optimization techniques
  • User experience considerations

The final product is a feature-rich productivity tool that goes beyond basic timer functionality, providing users with insights into their work patterns and helping them maintain focus throughout their day.

Key Metrics:

  • โฑ๏ธ Accurate timing with sub-second precision
  • ๐Ÿ“ฑ Fully responsive across all devices
  • ๐Ÿ”Š 5 different notification sounds
  • ๐Ÿ“Š Comprehensive statistics tracking
  • ๐ŸŽจ Dark/light theme support
  • ๐Ÿ’พ Persistent settings via localStorage
NOTE

This project reinforced my understanding of React fundamentals while introducing me to advanced concepts like Web Workers, browser APIs, and performance optimization techniques. Itโ€™s a testament to how modern web technologies can be used to create practical, user-friendly applications that solve real-world problems.

For inspiration and examples of similar productivity tools, check out these repositories:

facebook
/
react
Waiting for api.github.com...
00K
0K
0K
Waiting...
tailwindlabs
/
tailwindcss
Waiting for api.github.com...
00K
0K
0K
Waiting...
TIP

Building a Pomodoro timer is a great way to practice React concepts! Consider starting with a basic version and gradually adding features like statistics, themes, and sound notifications.

Try it out and let me know what you think! What features would you like to see in a productivity timer? ๐Ÿค”


View the live demo and source code on my portfolio. Feedback and suggestions are always welcome! ๐Ÿ’ฌ

ยฉ Copyright @ 2025. All Rights Reserved by Jack Phat

Powered by Astro & Inspried by Fuwari