timeflow-timer-pro / script.js
7yd's picture
I want to build a simple timer-based web application with a clean and minimal UI. Here are the required features and behavior:
77402e7 verified
Raw
History Blame Contribute Delete
12.6 kB
// 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();
};