Pomodoro Web App (Flask) in Python
About the project: This is a project for Pomodoro Web App using the Flask framework in a single, self-contained Python file. This file will contain all the necessary Python code for the backend, as well as the HTML, CSS, and JavaScript for the user interface.
This project is a great way to demonstrate how to build a full-stack web application in a single file, perfect for quick deployment and learning.
Instructions
- Install Flask: This project requires the Flask library. You'll need to install it first if you don't have it already.
pip install Flask
- Save the Code: Save the code above as a file named flask_app.py.
- Run the App: Open your terminal or command prompt, navigate to the directory where you saved the file, and run the script:
python flask_app.py
- Access the App: Open your web browser and navigate to http://127.0.0.1:5000. You will see the Pomodoro timer interface.
The app uses a separate thread on the server to manage the timer, allowing the front end to poll for the current state.
This ensures the timer remains accurate even if the user refreshes the page or their internet connection is briefly interrupted.
You can easily modify the HTML template and settings to customize the app further.
Project Level: Intermediate
You can directly copy the below snippet code with the help of green copy button, paste it and run it in any Python editor you have.
Steps: Follow these stepsStep 1: Copy below code using green 'copy' button.
Step 2: Paste the code on your chosen editor.
Step 3: Save the code with filename and .py extention.
Step 4: Run (Press F5 if using python IDLE).
# flask_app.py
from flask import Flask, render_template_string, request, jsonify
import json
import threading
import time
# --- Flask App Configuration ---
app = Flask(__name__)
# --- In-Memory State for the Timer ---
# In a real-world app, you might use a database or a more persistent storage.
# For this simple single-file app, we'll keep the timer state in memory.
timer_state = {
'running': False,
'current_task': 'Pomodoro',
'time_left_seconds': 25 * 60,
'pomodoro_duration': 25,
'short_break_duration': 5,
'long_break_duration': 15,
'completed_pomodoros': 0
}
# --- Timer Thread ---
def timer_worker():
"""Worker function to run the timer in a separate thread."""
global timer_state
while True:
if timer_state['running']:
if timer_state['time_left_seconds'] > 0:
timer_state['time_left_seconds'] -= 1
else:
# Timer has reached zero, handle state transition
timer_state['running'] = False
current_task = timer_state['current_task']
if current_task == 'Pomodoro':
timer_state['completed_pomodoros'] += 1
if timer_state['completed_pomodoros'] % 4 == 0:
timer_state['current_task'] = 'Long Break'
timer_state['time_left_seconds'] = timer_state['long_break_duration'] * 60
else:
timer_state['current_task'] = 'Short Break'
timer_state['time_left_seconds'] = timer_state['short_break_duration'] * 60
else:
# Break is over, go back to Pomodoro
timer_state['current_task'] = 'Pomodoro'
timer_state['time_left_seconds'] = timer_state['pomodoro_duration'] * 60
# Sleep for a second to avoid busy-waiting
time.sleep(1)
# Start the timer worker thread
timer_thread = threading.Thread(target=timer_worker, daemon=True)
timer_thread.start()
# --- HTML Template (stored as a string) ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pomodoro Timer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f3f4f6;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-xl p-8 transform transition-all duration-300 hover:shadow-2xl">
<h1 class="text-3xl sm:text-4xl font-bold text-center text-gray-800 mb-6">Pomodoro Timer</h1>
<div class="flex justify-center items-center space-x-2 sm:space-x-4 mb-8">
<button id="pomodoro-btn" class="px-4 py-2 sm:px-6 sm:py-3 rounded-full text-sm sm:text-base font-medium transition-colors duration-200">Pomodoro</button>
<button id="short-break-btn" class="px-4 py-2 sm:px-6 sm:py-3 rounded-full text-sm sm:text-base font-medium transition-colors duration-200">Short Break</button>
<button id="long-break-btn" class="px-4 py-2 sm:px-6 sm:py-3 rounded-full text-sm sm:text-base font-medium transition-colors duration-200">Long Break</button>
</div>
<div class="text-center mb-6">
<div id="timer-display" class="text-6xl sm:text-8xl font-bold text-gray-900 leading-none">25:00</div>
<p id="status-text" class="mt-2 text-lg sm:text-xl text-gray-600 transition-colors duration-200"></p>
</div>
<div class="flex justify-center items-center space-x-4 mb-8">
<button id="start-pause-btn" class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-full shadow-lg transform transition-transform duration-200 active:scale-95">Start</button>
<button id="reset-btn" class="px-6 py-3 bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold rounded-full shadow-lg transform transition-transform duration-200 active:scale-95">Reset</button>
</div>
<div class="flex items-center justify-center mt-6 text-gray-600">
<span class="mr-2">Completed Pomodoros:</span>
<span id="completed-pomodoros" class="font-bold text-gray-900">0</span>
</div>
<div class="mt-8 pt-6 border-t border-gray-200">
<h2 class="text-xl font-bold text-gray-800 mb-4 text-center">Settings</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center">
<div>
<label for="pomodoro-input" class="block text-sm font-medium text-gray-700 mb-1">Pomodoro</label>
<div class="relative rounded-md shadow-sm">
<input type="number" id="pomodoro-input" min="1" max="60" value="25" class="block w-full rounded-md border-gray-300 pl-3 pr-10 py-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">min</span>
</div>
</div>
</div>
<div>
<label for="short-break-input" class="block text-sm font-medium text-gray-700 mb-1">Short Break</label>
<div class="relative rounded-md shadow-sm">
<input type="number" id="short-break-input" min="1" max="15" value="5" class="block w-full rounded-md border-gray-300 pl-3 pr-10 py-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">min</span>
</div>
</div>
</div>
<div>
<label for="long-break-input" class="block text-sm font-medium text-gray-700 mb-1">Long Break</label>
<div class="relative rounded-md shadow-sm">
<input type="number" id="long-break-input" min="1" max="30" value="15" class="block w-full rounded-md border-gray-300 pl-3 pr-10 py-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm">min</span>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-center">
<button id="save-settings-btn" class="px-6 py-2 bg-green-500 hover:bg-green-600 text-white font-medium rounded-full shadow-md transform transition-transform duration-200 active:scale-95">Save Settings</button>
</div>
</div>
</div>
<script>
const timerDisplay = document.getElementById('timer-display');
const startPauseBtn = document.getElementById('start-pause-btn');
const resetBtn = document.getElementById('reset-btn');
const statusText = document.getElementById('status-text');
const completedPomodoros = document.getElementById('completed-pomodoros');
const pomodoroBtn = document.getElementById('pomodoro-btn');
const shortBreakBtn = document.getElementById('short-break-btn');
const longBreakBtn = document.getElementById('long-break-btn');
const pomodoroInput = document.getElementById('pomodoro-input');
const shortBreakInput = document.getElementById('short-break-input');
const longBreakInput = document.getElementById('long-break-input');
const saveSettingsBtn = document.getElementById('save-settings-btn');
let currentState = {
task: 'Pomodoro',
time: 25 * 60,
running: false,
completed: 0,
pomodoroDuration: 25 * 60,
shortBreakDuration: 5 * 60,
longBreakDuration: 15 * 60
};
let timerInterval = null;
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
function updateDisplay() {
timerDisplay.textContent = formatTime(currentState.time);
startPauseBtn.textContent = currentState.running ? 'Pause' : 'Start';
completedPomodoros.textContent = currentState.completed;
// Update button styles
const buttons = [pomodoroBtn, shortBreakBtn, longBreakBtn];
buttons.forEach(btn => {
btn.classList.remove('bg-indigo-600', 'text-white');
btn.classList.add('bg-gray-200', 'text-gray-800');
});
if (currentState.task === 'Pomodoro') {
pomodoroBtn.classList.remove('bg-gray-200', 'text-gray-800');
pomodoroBtn.classList.add('bg-indigo-600', 'text-white');
statusText.textContent = "Time to Focus";
} else if (currentState.task === 'Short Break') {
shortBreakBtn.classList.remove('bg-gray-200', 'text-gray-800');
shortBreakBtn.classList.add('bg-indigo-600', 'text-white');
statusText.textContent = "Short Break";
} else if (currentState.task === 'Long Break') {
longBreakBtn.classList.remove('bg-gray-200', 'text-gray-800');
longBreakBtn.classList.add('bg-indigo-600', 'text-white');
statusText.textContent = "Long Break";
}
}
function switchTask(task) {
if (currentState.running) {
// Confirm with user if timer is running
if (!confirm("A timer is running. Are you sure you want to switch?")) {
return;
}
}
currentState.running = false;
currentState.task = task;
if (task === 'Pomodoro') {
currentState.time = currentState.pomodoroDuration;
} else if (task === 'Short Break') {
currentState.time = currentState.shortBreakDuration;
} else if (task === 'Long Break') {
currentState.time = currentState.longBreakDuration;
}
fetch('/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentState)
});
updateDisplay();
}
function startPauseTimer() {
currentState.running = !currentState.running;
fetch('/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentState)
});
updateDisplay();
}
function resetTimer() {
currentState.running = false;
currentState.time = currentState.pomodoroDuration;
currentState.completed = 0; // Reset completed count on full reset
currentState.task = 'Pomodoro';
fetch('/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentState)
});
updateDisplay();
}
function saveSettings() {
const pDuration = parseInt(pomodoroInput.value);
const sDuration = parseInt(shortBreakInput.value);
const lDuration = parseInt(longBreakInput.value);
if (pDuration <= 0 || sDuration <= 0 || lDuration <= 0) {
alert("Durations must be positive numbers.");
return;
}
currentState.pomodoroDuration = pDuration * 60;
currentState.shortBreakDuration = sDuration * 60;
currentState.longBreakDuration = lDuration * 60;
// Apply new settings to the current task if timer is not running
if (!currentState.running) {
if (currentState.task === 'Pomodoro') {
currentState.time = currentState.pomodoroDuration;
} else if (currentState.task === 'Short Break') {
currentState.time = currentState.shortBreakDuration;
} else if (currentState.task === 'Long Break') {
currentState.time = currentState.longBreakDuration;
}
}
fetch('/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentState)
}).then(() => {
alert("Settings saved successfully!");
updateDisplay();
});
}
// Event Listeners
startPauseBtn.addEventListener('click', startPauseTimer);
resetBtn.addEventListener('click', resetTimer);
pomodoroBtn.addEventListener('click', () => switchTask('Pomodoro'));
shortBreakBtn.addEventListener('click', () => switchTask('Short Break'));
longBreakBtn.addEventListener('click', () => switchTask('Long Break'));
saveSettingsBtn.addEventListener('click', saveSettings);
// Fetch initial state and update display
function fetchState() {
fetch('/state')
.then(response => response.json())
.then(data => {
currentState.task = data.current_task;
currentState.time = data.time_left_seconds;
currentState.running = data.running;
currentState.completed = data.completed_pomodoros;
currentState.pomodoroDuration = data.pomodoro_duration * 60;
currentState.shortBreakDuration = data.short_break_duration * 60;
currentState.longBreakDuration = data.long_break_duration * 60;
pomodoroInput.value = data.pomodoro_duration;
shortBreakInput.value = data.short_break_duration;
longBreakInput.value = data.long_break_duration;
updateDisplay();
});
}
fetchState();
setInterval(fetchState, 1000); // Poll the server for updates every second
</script>
</body>
</html>
"""
# --- Flask Routes ---
@app.route('/')
def index():
"""Serves the main HTML page for the Pomodoro timer."""
return render_template_string(HTML_TEMPLATE)
@app.route('/state', methods=['GET', 'POST'])
def handle_state():
"""
Endpoint to get and update the timer's state.
GET: Returns the current state as JSON.
POST: Updates the state based on the received JSON data.
"""
global timer_state
if request.method == 'POST':
data = request.json
if data:
# We'll use this to synchronize the client state with the server
if 'running' in data:
timer_state['running'] = data['running']
if 'task' in data:
timer_state['current_task'] = data['task']
if 'time' in data:
timer_state['time_left_seconds'] = data['time']
if 'completed' in data:
timer_state['completed_pomodoros'] = data['completed']
if 'pomodoroDuration' in data:
timer_state['pomodoro_duration'] = data['pomodoroDuration'] / 60
if 'shortBreakDuration' in data:
timer_state['short_break_duration'] = data['shortBreakDuration'] / 60
if 'longBreakDuration' in data:
timer_state['long_break_duration'] = data['longBreakDuration'] / 60
return jsonify({'status': 'ok'})
# GET request
return jsonify(timer_state)
if __name__ == '__main__':
# This server is for development purposes only.
# It runs a lightweight server accessible on all network interfaces.
# In a production environment, you would use a more robust WSGI server.
app.run(debug=True, host='0.0.0.0', port=5000)
← Back to Projects