A simple pomodoro timer with many cool features
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.
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.
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
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
};
};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 };
};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>
);
};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>
);
};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 };
};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 };
};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>
);
};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>
);
});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];
};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;
};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;
}
};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;
}
}
}Building this Pomodoro timer was an excellent learning experience that combined practical productivity needs with modern web development techniques. The project demonstrates:
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:
NOTEThis 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:
TIPBuilding 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! ๐ฌ