padomoro-timer / index.html
adeism's picture
Add 1 files
29d0808 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Serene Pomodoro Timer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Poppins', sans-serif;
background-image: url('https://images.unsplash.com/photo-1500382018106-4073a5091250?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80');
background-size: cover;
background-position: center;
background-attachment: fixed;
transition: background-color 0.5s ease;
}
.timer-container {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.85);
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
}
.progress-ring__circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.tab-active {
position: relative;
}
.tab-active::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 3px;
background-color: #4F46E5;
border-radius: 3px;
}
.settings-panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.settings-open {
max-height: 600px; /* Increased to accommodate sound options */
overflow-y: auto; /* Add scroll if needed */
}
/* Custom range slider */
input[type="range"] {
-webkit-appearance: none;
height: 6px;
background: #E5E7EB;
border-radius: 5px;
background-image: linear-gradient(#4F46E5, #4F46E5);
background-size: 0% 100%;
background-repeat: no-repeat;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #4F46E5;
cursor: pointer;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 5px rgba(79, 70, 229, 0.5);
}
/* Toggle switch */
.toggle-checkbox:checked {
right: 0;
border-color: #4F46E5;
}
.toggle-checkbox:checked + .toggle-label {
background-color: #4F46E5;
}
/* Nature sounds selector */
.sound-option {
transition: all 0.2s ease;
}
.sound-option:hover {
transform: translateY(-2px);
}
.sound-option.selected {
border-color: #4F46E5;
background-color: rgba(79, 70, 229, 0.1);
}
/* Animation for timer buttons */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.pulse {
animation: pulse 2s infinite;
}
/* Custom scrollbar for settings panel */
.settings-panel::-webkit-scrollbar {
width: 6px;
}
.settings-panel::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.settings-panel::-webkit-scrollbar-thumb {
background: rgba(79, 70, 229, 0.3);
border-radius: 3px;
}
.settings-panel::-webkit-scrollbar-thumb:hover {
background: rgba(79, 70, 229, 0.5);
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="timer-container rounded-2xl p-8 w-full max-w-md">
<!-- Timer Mode Tabs -->
<div class="flex justify-center mb-8">
<div class="flex space-x-6 bg-gray-100 p-1 rounded-full">
<button id="pomodoro-tab" class="tab-active px-4 py-2 text-indigo-600 font-medium focus:outline-none">
Pomodoro
</button>
<button id="short-break-tab" class="px-4 py-2 text-gray-600 font-medium focus:outline-none">
Short Break
</button>
<button id="long-break-tab" class="px-4 py-2 text-gray-600 font-medium focus:outline-none">
Long Break
</button>
</div>
</div>
<!-- Timer Display -->
<div class="flex justify-center mb-8">
<div class="relative w-64 h-64">
<svg class="w-full h-full" viewBox="0 0 100 100">
<circle class="text-gray-200" stroke-width="6" stroke="currentColor" fill="transparent" r="45" cx="50" cy="50" />
<circle id="progress-ring" class="progress-ring__circle text-indigo-500" stroke-width="6" stroke-linecap="round" stroke="currentColor" fill="transparent" r="45" cx="50" cy="50" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<div id="time-display" class="text-5xl font-bold text-gray-800">25:00</div>
<button id="timer-control" class="mt-4 px-8 py-2 bg-indigo-600 text-white rounded-full font-medium focus:outline-none hover:bg-indigo-700 transition">
START
</button>
</div>
</div>
</div>
<!-- Current Session Info -->
<div id="session-info" class="text-center text-gray-600 mb-8">
<p>Session #1 • Focus Time</p>
</div>
<!-- Settings Button -->
<div class="flex justify-center mb-4">
<button id="settings-button" class="text-gray-600 hover:text-indigo-600 focus:outline-none transition">
<i class="fas fa-cog text-2xl"></i>
</button>
</div>
<!-- Settings Panel -->
<div id="settings-panel" class="settings-panel bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Timer Settings</h3>
<div class="space-y-6">
<!-- Pomodoro Duration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Pomodoro (minutes)</label>
<div class="flex items-center space-x-4">
<input id="pomodoro-duration" type="range" min="5" max="60" value="25" class="w-full">
<span id="pomodoro-value" class="text-gray-700 font-medium w-10 text-center">25</span>
</div>
</div>
<!-- Short Break Duration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Short Break (minutes)</label>
<div class="flex items-center space-x-4">
<input id="short-break-duration" type="range" min="1" max="15" value="5" class="w-full">
<span id="short-break-value" class="text-gray-700 font-medium w-10 text-center">5</span>
</div>
</div>
<!-- Long Break Duration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Long Break (minutes)</label>
<div class="flex items-center space-x-4">
<input id="long-break-duration" type="range" min="10" max="30" value="15" class="w-full">
<span id="long-break-value" class="text-gray-700 font-medium w-10 text-center">15</span>
</div>
</div>
<!-- Auto-start Breaks -->
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700">Auto-start Breaks</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="auto-start-breaks" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="auto-start-breaks" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- Auto-start Pomodoros -->
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700">Auto-start Pomodoros</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="auto-start-pomodoros" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="auto-start-pomodoros" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- Long Break Interval -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Long Break Interval (pomodoros)</label>
<div class="flex items-center space-x-4">
<input id="long-break-interval" type="range" min="2" max="8" value="4" class="w-full">
<span id="long-break-interval-value" class="text-gray-700 font-medium w-10 text-center">4</span>
</div>
</div>
<!-- Nature Sounds -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nature Sounds</label>
<div class="grid grid-cols-3 gap-3">
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer">
<i class="fas fa-wind text-xl text-gray-600 mb-1"></i>
<p class="text-xs">Forest</p>
</div>
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer selected">
<i class="fas fa-water text-xl text-gray-600 mb-1"></i>
<p class="text-xs">Rain</p>
</div>
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer">
<i class="fas fa-feather text-xl text-gray-600 mb-1"></i>
<p class="text-xs">Birds</p>
</div>
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer">
<i class="fas fa-fire text-xl text-gray-600 mb-1"></i>
<p class="text-xs">Fire</p>
</div>
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer">
<i class="fas fa-umbrella-beach text-xl text-gray-600 mb-1"></i>
<p class="text-xs">Waves</p>
</div>
<div class="sound-option border rounded-lg p-3 text-center cursor-pointer">
<i class="fas fa-volume-mute text-xl text-gray-600 mb-1"></i>
<p class="text-xs">None</p>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="save-settings" class="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium focus:outline-none hover:bg-indigo-700 transition">
Save Settings
</button>
</div>
</div>
</div>
<audio id="tick-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-clock-countdown-bleeps-916.mp3" preload="auto"></audio>
<audio id="alarm-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-alarm-digital-clock-beep-989.mp3" preload="auto"></audio>
<audio id="nature-sound" loop src="https://assets.mixkit.co/sfx/preview/mixkit-rain-loop-1243.mp3" preload="auto"></audio>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const timeDisplay = document.getElementById('time-display');
const timerControl = document.getElementById('timer-control');
const sessionInfo = document.getElementById('session-info');
const progressRing = document.getElementById('progress-ring');
const settingsButton = document.getElementById('settings-button');
const settingsPanel = document.getElementById('settings-panel');
const saveSettings = document.getElementById('save-settings');
const pomodoroTab = document.getElementById('pomodoro-tab');
const shortBreakTab = document.getElementById('short-break-tab');
const longBreakTab = document.getElementById('long-break-tab');
const tickSound = document.getElementById('tick-sound');
const alarmSound = document.getElementById('alarm-sound');
const natureSound = document.getElementById('nature-sound');
// Timer state
let timer;
let isRunning = false;
let timeLeft = 25 * 60; // 25 minutes in seconds
let totalTime = 25 * 60;
let currentMode = 'pomodoro'; // 'pomodoro', 'shortBreak', 'longBreak'
let sessionsCompleted = 0;
let longBreakInterval = 4;
let autoStartBreaks = false;
let autoStartPomodoros = false;
let natureSoundEnabled = true;
// Initialize UI
updateTimeDisplay();
updateProgressRing();
setupRangeInputs();
setupSoundOptions();
// Event Listeners
timerControl.addEventListener('click', toggleTimer);
settingsButton.addEventListener('click', toggleSettings);
saveSettings.addEventListener('click', saveTimerSettings);
pomodoroTab.addEventListener('click', () => switchMode('pomodoro'));
shortBreakTab.addEventListener('click', () => switchMode('shortBreak'));
longBreakTab.addEventListener('click', () => switchMode('longBreak'));
// Timer Functions
function toggleTimer() {
if (isRunning) {
pauseTimer();
} else {
startTimer();
}
}
function startTimer() {
if (!isRunning) {
isRunning = true;
timerControl.textContent = 'PAUSE';
timerControl.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
timerControl.classList.add('bg-amber-500', 'hover:bg-amber-600');
if (natureSoundEnabled) {
natureSound.play().catch(e => console.log("Audio play failed:", e));
}
timer = setInterval(() => {
timeLeft--;
updateTimeDisplay();
updateProgressRing();
// Play tick sound every second
if (timeLeft > 0 && timeLeft % 60 === 0) {
tickSound.currentTime = 0;
tickSound.play().catch(e => console.log("Tick sound play failed:", e));
}
if (timeLeft <= 0) {
clearInterval(timer);
timerComplete();
}
}, 1000);
}
}
function pauseTimer() {
if (isRunning) {
isRunning = false;
timerControl.textContent = 'RESUME';
timerControl.classList.remove('bg-amber-500', 'hover:bg-amber-600');
timerControl.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
natureSound.pause();
clearInterval(timer);
}
}
function timerComplete() {
isRunning = false;
alarmSound.play().catch(e => console.log("Alarm sound play failed:", e));
natureSound.pause();
if (currentMode === 'pomodoro') {
sessionsCompleted++;
// Check if it's time for a long break
if (sessionsCompleted % longBreakInterval === 0) {
if (autoStartBreaks) {
setTimeout(() => switchMode('longBreak'), 1000);
} else {
timerControl.textContent = 'START LONG BREAK';
updateSessionInfo(`Pomodoro completed! Take a long break.`);
}
} else {
if (autoStartBreaks) {
setTimeout(() => switchMode('shortBreak'), 1000);
} else {
timerControl.textContent = 'START SHORT BREAK';
updateSessionInfo(`Pomodoro completed! Take a short break.`);
}
}
} else {
if (autoStartPomodoros) {
setTimeout(() => switchMode('pomodoro'), 1000);
} else {
timerControl.textContent = 'START POMODORO';
updateSessionInfo(`Break completed! Ready for next pomodoro?`);
}
}
timerControl.classList.remove('bg-amber-500', 'hover:bg-amber-600');
timerControl.classList.add('bg-green-500', 'hover:bg-green-600', 'pulse');
}
function switchMode(mode) {
// Reset timer state
clearInterval(timer);
isRunning = false;
timerControl.classList.remove('pulse', 'bg-green-500', 'hover:bg-green-600', 'bg-amber-500', 'hover:bg-amber-600');
timerControl.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
timerControl.textContent = 'START';
natureSound.pause();
// Update UI for selected tab
pomodoroTab.classList.remove('tab-active', 'text-indigo-600');
pomodoroTab.classList.add('text-gray-600');
shortBreakTab.classList.remove('tab-active', 'text-indigo-600');
shortBreakTab.classList.add('text-gray-600');
longBreakTab.classList.remove('tab-active', 'text-indigo-600');
longBreakTab.classList.add('text-gray-600');
// Set new mode
currentMode = mode;
// Set time based on mode
if (mode === 'pomodoro') {
timeLeft = parseInt(document.getElementById('pomodoro-duration').value) * 60;
totalTime = timeLeft;
pomodoroTab.classList.add('tab-active', 'text-indigo-600');
updateSessionInfo(`Session #${Math.floor(sessionsCompleted/longBreakInterval) + 1} • Focus Time`);
} else if (mode === 'shortBreak') {
timeLeft = parseInt(document.getElementById('short-break-duration').value) * 60;
totalTime = timeLeft;
shortBreakTab.classList.add('tab-active', 'text-indigo-600');
updateSessionInfo(`Short Break • Relax`);
} else if (mode === 'longBreak') {
timeLeft = parseInt(document.getElementById('long-break-duration').value) * 60;
totalTime = timeLeft;
longBreakTab.classList.add('tab-active', 'text-indigo-600');
updateSessionInfo(`Long Break • Recharge`);
}
updateTimeDisplay();
updateProgressRing();
// Auto-start if enabled
if ((mode === 'shortBreak' || mode === 'longBreak') && autoStartBreaks) {
startTimer();
} else if (mode === 'pomodoro' && autoStartPomodoros) {
startTimer();
}
}
function updateTimeDisplay() {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
timeDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
function updateProgressRing() {
const circumference = 2 * Math.PI * 45;
const offset = circumference - (timeLeft / totalTime) * circumference;
progressRing.style.strokeDasharray = `${circumference} ${circumference}`;
progressRing.style.strokeDashoffset = offset;
// Change color based on time left
if (timeLeft / totalTime < 0.25) {
progressRing.classList.remove('text-indigo-500', 'text-amber-500');
progressRing.classList.add('text-red-500');
} else if (timeLeft / totalTime < 0.5) {
progressRing.classList.remove('text-indigo-500', 'text-red-500');
progressRing.classList.add('text-amber-500');
} else {
progressRing.classList.remove('text-amber-500', 'text-red-500');
progressRing.classList.add('text-indigo-500');
}
}
function updateSessionInfo(text) {
sessionInfo.textContent = text;
}
// Settings Functions
function toggleSettings() {
settingsPanel.classList.toggle('settings-open');
settingsButton.innerHTML = settingsPanel.classList.contains('settings-open') ?
'<i class="fas fa-times text-2xl"></i>' : '<i class="fas fa-cog text-2xl"></i>';
}
function setupRangeInputs() {
// Pomodoro duration
const pomodoroDuration = document.getElementById('pomodoro-duration');
const pomodoroValue = document.getElementById('pomodoro-value');
pomodoroDuration.addEventListener('input', () => {
pomodoroValue.textContent = pomodoroDuration.value;
updateBackgroundSize(pomodoroDuration);
});
updateBackgroundSize(pomodoroDuration);
// Short break duration
const shortBreakDuration = document.getElementById('short-break-duration');
const shortBreakValue = document.getElementById('short-break-value');
shortBreakDuration.addEventListener('input', () => {
shortBreakValue.textContent = shortBreakDuration.value;
updateBackgroundSize(shortBreakDuration);
});
updateBackgroundSize(shortBreakDuration);
// Long break duration
const longBreakDuration = document.getElementById('long-break-duration');
const longBreakValue = document.getElementById('long-break-value');
longBreakDuration.addEventListener('input', () => {
longBreakValue.textContent = longBreakDuration.value;
updateBackgroundSize(longBreakDuration);
});
updateBackgroundSize(longBreakDuration);
// Long break interval
const longBreakIntervalInput = document.getElementById('long-break-interval');
const longBreakIntervalValue = document.getElementById('long-break-interval-value');
longBreakIntervalInput.addEventListener('input', () => {
longBreakIntervalValue.textContent = longBreakIntervalInput.value;
updateBackgroundSize(longBreakIntervalInput);
});
updateBackgroundSize(longBreakIntervalInput);
}
function updateBackgroundSize(rangeInput) {
const value = rangeInput.value;
const min = rangeInput.min ? rangeInput.min : 0;
const max = rangeInput.max ? rangeInput.max : 100;
const percentage = (value - min) / (max - min) * 100;
rangeInput.style.backgroundSize = `${percentage}% 100%`;
}
function setupSoundOptions() {
const soundOptions = document.querySelectorAll('.sound-option');
soundOptions.forEach(option => {
option.addEventListener('click', function() {
soundOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
// Change nature sound based on selection
const soundType = this.querySelector('p').textContent.toLowerCase();
changeNatureSound(soundType);
});
});
}
function changeNatureSound(soundType) {
if (soundType === 'none') {
natureSoundEnabled = false;
natureSound.pause();
return;
}
natureSoundEnabled = true;
let soundSrc = '';
switch(soundType) {
case 'forest':
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-forest-stream-1353.mp3';
break;
case 'rain':
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-rain-loop-1243.mp3';
break;
case 'birds':
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-birds-chirping-1480.mp3';
break;
case 'fire':
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-crackling-fireplace-1352.mp3';
break;
case 'waves':
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-waves-coming-to-shore-1170.mp3';
break;
default:
soundSrc = 'https://assets.mixkit.co/sfx/preview/mixkit-rain-loop-1243.mp3';
}
natureSound.src = soundSrc;
if (isRunning) {
natureSound.play().catch(e => console.log("Nature sound play failed:", e));
}
}
function saveTimerSettings() {
// Get values from inputs
const pomodoroMinutes = parseInt(document.getElementById('pomodoro-duration').value);
const shortBreakMinutes = parseInt(document.getElementById('short-break-duration').value);
const longBreakMinutes = parseInt(document.getElementById('long-break-duration').value);
longBreakInterval = parseInt(document.getElementById('long-break-interval').value);
autoStartBreaks = document.getElementById('auto-start-breaks').checked;
autoStartPomodoros = document.getElementById('auto-start-pomodoros').checked;
// Update current timer if needed
if (!isRunning) {
if (currentMode === 'pomodoro') {
timeLeft = pomodoroMinutes * 60;
totalTime = timeLeft;
} else if (currentMode === 'shortBreak') {
timeLeft = shortBreakMinutes * 60;
totalTime = timeLeft;
} else if (currentMode === 'longBreak') {
timeLeft = longBreakMinutes * 60;
totalTime = timeLeft;
}
updateTimeDisplay();
updateProgressRing();
}
// Close settings
toggleSettings();
// Show confirmation
const originalText = saveSettings.textContent;
saveSettings.textContent = 'Saved!';
saveSettings.classList.remove('bg-indigo-600');
saveSettings.classList.add('bg-green-500');
setTimeout(() => {
saveSettings.textContent = originalText;
saveSettings.classList.remove('bg-green-500');
saveSettings.classList.add('bg-indigo-600');
}, 2000);
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=adeism/padomoro-timer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>