Spaces:
Running
Running
| // Shared JavaScript across all pages | |
| class TimerManager { | |
| constructor() { | |
| this.timers = JSON.parse(localStorage.getItem('timers')) || []; | |
| this.selectedDuration = 3600; // Default 1 hour in seconds | |
| this.init(); | |
| } | |
| init() { | |
| this.bindEvents(); | |
| this.renderTimers(); | |
| this.startTimerUpdates(); | |
| } | |
| bindEvents() { | |
| // Modal controls | |
| document.getElementById('addTimerBtn').addEventListener('click', () => this.openModal()); | |
| document.getElementById('closeModal').addEventListener('click', () => this.closeModal()); | |
| // Duration selection | |
| document.querySelectorAll('.duration-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => this.selectDuration(e.target)); | |
| }); | |
| // Form submission | |
| document.getElementById('timerForm').addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| this.createTimer(true); | |
| }); | |
| // Add only button | |
| document.getElementById('addOnlyBtn').addEventListener('click', () => { | |
| this.createTimer(false); | |
| }); | |
| // Toggle controls | |
| document.getElementById('startImmediately').addEventListener('change', (e) => { | |
| this.toggleDelaySection(!e.target.checked); | |
| }); | |
| // Close modal on outside click | |
| document.getElementById('timerModal').addEventListener('click', (e) => { | |
| if (e.target.id === 'timerModal') { | |
| this.closeModal(); | |
| } | |
| }); | |
| } | |
| openModal() { | |
| const modal = document.getElementById('timerModal'); | |
| modal.classList.remove('hidden'); | |
| setTimeout(() => { | |
| modal.querySelector('div').classList.add('modal-open'); | |
| }, 10); | |
| } | |
| closeModal() { | |
| const modal = document.getElementById('timerModal'); | |
| modal.querySelector('div').classList.remove('modal-open'); | |
| setTimeout(() => { | |
| modal.classList.add('hidden'); | |
| this.resetForm(); | |
| }, 300); | |
| } | |
| selectDuration(button) { | |
| // Remove active class from all buttons | |
| document.querySelectorAll('.duration-btn').forEach(btn => { | |
| btn.classList.remove('bg-blue-500', 'text-white'); | |
| btn.classList.add('bg-slate-100', 'text-slate-700'); | |
| }); | |
| // Add active class to clicked button | |
| button.classList.remove('bg-slate-100', 'text-slate-700'); | |
| button.classList.add('bg-blue-500', 'text-white'); | |
| this.selectedDuration = parseInt(button.dataset.duration); | |
| } | |
| toggleDelaySection(immediate) { | |
| const delaySection = document.getElementById('delaySection'); | |
| const delayInput = document.getElementById('delayTime'); | |
| if (immediate) { | |
| delaySection.classList.add('opacity-50', 'pointer-events-none'); | |
| delayInput.disabled = true; | |
| } else { | |
| delaySection.classList.remove('opacity-50', 'pointer-events-none'); | |
| delayInput.disabled = false; | |
| } | |
| } | |
| resetForm() { | |
| document.getElementById('timerForm').reset(); | |
| document.getElementById('delayTime').disabled = false; | |
| document.getElementById('delaySection').classList.remove('opacity-50', 'pointer-events-none'); | |
| // Reset duration buttons to default | |
| document.querySelectorAll('.duration-btn').forEach(btn => { | |
| btn.classList.remove('bg-blue-500', 'text-white'); | |
| btn.classList.add('bg-slate-100', 'text-slate-700'); | |
| }); | |
| // Set first duration button as active | |
| const firstBtn = document.querySelector('.duration-btn'); | |
| firstBtn.classList.remove('bg-slate-100', 'text-slate-700'); | |
| firstBtn.classList.add('bg-blue-500', 'text-white'); | |
| this.selectedDuration = 3600; | |
| } | |
| createTimer(startImmediately) { | |
| const title = document.getElementById('timerTitle').value.trim(); | |
| const delayMinutes = document.getElementById('delayTime').value; | |
| const addWithoutStart = document.getElementById('addWithoutStart').checked; | |
| if (!title) { | |
| alert('Please enter a timer title'); | |
| return; | |
| } | |
| const timer = { | |
| id: Date.now().toString(), | |
| title: title, | |
| duration: this.selectedDuration, | |
| delay: parseInt(delayMinutes) * 60, // Convert to seconds | |
| status: addWithoutStart ? 'paused' : (startImmediately ? 'delayed' : 'paused'), | |
| startTime: startImmediately ? Date.now() : null, | |
| remaining: this.selectedDuration, | |
| createdAt: Date.now() | |
| }; | |
| this.timers.push(timer); | |
| this.saveTimers(); | |
| this.renderTimers(); | |
| this.closeModal(); | |
| // If starting immediately and no delay, begin countdown | |
| if (startImmediately && !addWithoutStart && delayMinutes === '0') { | |
| timer.status = 'running'; | |
| timer.startTime = Date.now(); | |
| } | |
| } | |
| startTimer(timerId) { | |
| const timer = this.timers.find(t => t.id === timerId); | |
| if (timer) { | |
| timer.status = timer.delay > 0 ? 'delayed' : 'running'; | |
| timer.startTime = Date.now(); | |
| this.saveTimers(); | |
| this.renderTimers(); | |
| } | |
| } | |
| deleteTimer(timerId) { | |
| this.timers = this.timers.filter(t => t.id !== timerId); | |
| this.saveTimers(); | |
| this.renderTimers(); | |
| } | |
| updateTimerRemaining() { | |
| const now = Date.now(); | |
| this.timers.forEach(timer => { | |
| if (timer.status === 'running' && timer.startTime) { | |
| const elapsed = Math.floor((now - timer.startTime) / 1000); | |
| timer.remaining = Math.max(0, timer.duration - elapsed); | |
| if (timer.remaining === 0) { | |
| this.completeTimer(timer.id); | |
| } | |
| } else if (timer.status === 'delayed' && timer.startTime) { | |
| const elapsed = Math.floor((now - timer.startTime) / 1000); | |
| if (elapsed >= timer.delay) { | |
| timer.status = 'running'; | |
| timer.startTime = now; // Reset start time for actual countdown | |
| } | |
| }); | |
| this.renderTimers(); | |
| } | |
| completeTimer(timerId) { | |
| const timer = this.timers.find(t => t.id === timerId); | |
| if (timer) { | |
| timer.status = 'completed'; | |
| this.saveTimers(); | |
| this.renderTimers(); | |
| this.playAlertSound(); | |
| } | |
| } | |
| playAlertSound() { | |
| const audio = document.getElementById('alertSound'); | |
| audio.play().catch(e => console.log('Audio play failed:', e)); | |
| } | |
| formatTime(seconds) { | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = seconds % 60; | |
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| renderTimers() { | |
| const container = document.getElementById('timersContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| if (this.timers.length === 0) { | |
| container.classList.add('hidden'); | |
| emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| container.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| container.innerHTML = this.timers.map(timer => this.createTimerHTML(timer)).join(''); | |
| // Re-bind events for dynamically created elements | |
| this.bindTimerEvents(); | |
| } | |
| createTimerHTML(timer) { | |
| const isCompleted = timer.status === 'completed'; | |
| const isPaused = timer.status === 'paused'; | |
| const isDelayed = timer.status === 'delayed'; | |
| let statusText = ''; | |
| let statusIcon = ''; | |
| let statusClass = ''; | |
| if (isCompleted) { | |
| statusText = 'Finished'; | |
| statusIcon = 'check-circle'; | |
| statusClass = 'bg-green-100 text-green-800'; | |
| } else if (isPaused) { | |
| statusText = 'Paused'; | |
| statusIcon = 'pause'; | |
| statusClass = 'bg-yellow-100 text-yellow-800'; | |
| } else if (isDelayed) { | |
| statusText = 'Delayed'; | |
| statusIcon = 'clock'; | |
| statusClass = 'bg-blue-100 text-blue-800'; | |
| } else { | |
| statusText = 'Running'; | |
| statusIcon = 'play'; | |
| statusClass = 'bg-blue-100 text-blue-800'; | |
| } | |
| return ` | |
| <div class="timer-card bg-white rounded-2xl shadow-lg p-6 border border-slate-200 ${isCompleted ? 'timer-finished' : ''}"> | |
| <div class="flex justify-between items-start mb-4"> | |
| <h3 class="text-lg font-semibold text-slate-800">${timer.title}</h3> | |
| <div class="relative"> | |
| <button class="timer-menu-btn text-slate-400 hover:text-slate-600 transition-colors" data-timer-id="${timer.id}"> | |
| <i data-feather="more-vertical"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="text-center mb-4"> | |
| <div class="text-3xl font-mono font-bold text-slate-800"> | |
| ${this.formatTime(timer.remaining)} | |
| </div> | |
| <div class="flex items-center justify-center gap-2 mb-4"> | |
| <span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium ${statusClass}"> | |
| <i data-feather="${statusIcon}" class="w-4 h-4"></i> | |
| ${statusText} | |
| </span> | |
| </div> | |
| <div class="flex gap-2"> | |
| ${isPaused ? ` | |
| <button class="flex-1 bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-xl font-medium transition-all duration-200 transform hover:scale-105 start-timer-btn" data-timer-id="${timer.id}"> | |
| <i data-feather="play" class="w-4 h-4 mr-2"></i> | |
| Start | |
| </button> | |
| ` : ''} | |
| ${!isCompleted ? ` | |
| <button class="delete-timer-btn bg-red-500 hover:bg-red-600 text-white p-2 rounded-xl transition-all duration-200 transform hover:scale-105" data-timer-id="${timer.id}"> | |
| <i data-feather="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| ` : ` | |
| <button class="flex-1 bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-xl font-medium transition-all duration-200 transform hover:scale-105 delete-timer-btn" data-timer-id="${timer.id}"> | |
| <i data-feather="trash-2" class="w-4 h-4 mr-2"></i> | |
| Delete | |
| </button> | |
| `} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| bindTimerEvents() { | |
| // Start timer buttons | |
| document.querySelectorAll('.start-timer-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const timerId = e.target.closest('.start-timer-btn').dataset.timerId; | |
| this.startTimer(timerId); | |
| }); | |
| }); | |
| // Delete timer buttons | |
| document.querySelectorAll('.delete-timer-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const timerId = e.target.closest('.delete-timer-btn').dataset.timerId; | |
| this.deleteTimer(timerId); | |
| }); | |
| }); | |
| // Timer menu buttons | |
| document.querySelectorAll('.timer-menu-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| this.showTimerMenu(e); | |
| }); | |
| }); | |
| } | |
| showTimerMenu(event) { | |
| // For now, just delete the timer when menu is clicked | |
| const timerId = event.target.closest('.timer-menu-btn').dataset.timerId; | |
| this.deleteTimer(timerId); | |
| } | |
| startTimerUpdates() { | |
| setInterval(() => { | |
| this.updateTimerRemaining(); | |
| }, 1000); | |
| } | |
| saveTimers() { | |
| localStorage.setItem('timers', JSON.stringify(this.timers)); | |
| } | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new TimerManager(); | |
| feather.replace(); | |
| }); | |
| // Re-run feather.replace() whenever timers are re-rendered | |
| const originalRenderTimers = TimerManager.prototype.renderTimers; | |
| TimerManager.prototype.renderTimers = function() { | |
| originalRenderTimers.call(this); | |
| feather.replace(); | |
| }; |