focus-flow-timer / index.html
suhas0's picture
remove tags option - Initial Deployment
d56af72 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Circular Timer with Progress Bar</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.glow {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 70%);
filter: blur(10px);
z-index: -1;
opacity: 0;
transition: opacity 0.3s;
}
.timer-container:hover .glow {
opacity: 1;
}
@keyframes progress {
0% { stroke-dashoffset: 283; }
100% { stroke-dashoffset: 0; }
}
.progress-circle {
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.clock-face {
box-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body class="bg-black min-h-screen flex flex-col items-center justify-center p-4">
<div class="max-w-md w-full mx-auto">
<h1 class="text-white text-3xl font-bold mb-4 text-center">Focus Flow Timer</h1>
<!-- Tag Management Section -->
<div class="mb-4 w-full max-w-md">
<div class="flex items-center justify-between mb-2">
<label class="block text-white">Select Tag</label>
<button id="manage-tags-btn" class="text-blue-400 hover:text-blue-300 text-sm">
Manage Tags
</button>
</div>
<select id="tags-input" class="w-full bg-gray-800 text-white rounded p-2">
<!-- Tags will be loaded dynamically -->
</select>
</div>
<!-- Tag Management Modal -->
<div id="tag-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-gray-800 rounded-lg p-6 w-full max-w-md">
<h3 class="text-white text-xl font-bold mb-4">Manage Tags</h3>
<div class="mb-4">
<input id="new-tag-input" type="text" placeholder="New tag name"
class="w-full bg-gray-700 text-white rounded p-2 mb-2">
<textarea id="tag-description" placeholder="Tag description (optional)"
class="w-full bg-gray-700 text-white rounded p-2 mb-2"></textarea>
<button id="add-tag-btn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded w-full">
Add Tag
</button>
</div>
<div id="tags-list" class="max-h-60 overflow-y-auto mb-4">
<!-- Tags will be listed here -->
</div>
<div class="flex justify-end space-x-2">
<button id="close-tag-modal" class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded">
Close
</button>
</div>
</div>
</div>
<div class="relative w-64 h-64 mx-auto mb-8 timer-container">
<div class="glow"></div>
<button id="fullscreen-btn" class="absolute top-2 right-2 bg-gray-800 bg-opacity-50 text-white p-1 rounded-full z-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
<!-- Progress bar circle -->
<svg class="absolute top-0 left-0 w-full h-full" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="#2d3748" stroke-width="8"/>
<circle id="progress-bar" class="progress-circle" cx="50" cy="50" r="45" fill="none" stroke="#ffffff" stroke-width="8" stroke-dasharray="283" stroke-dashoffset="283"/>
</svg>
<!-- Clock face -->
<div class="clock-face absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 bg-white rounded-full flex items-center justify-center">
<div id="timer-display" class="text-4xl font-bold">00:00:00</div>
</div>
</div>
<!-- Controls -->
<div class="bg-gray-900 rounded-lg p-6">
<div class="mb-4">
<label class="block text-white mb-2">Set Timer Duration</label>
<div class="grid grid-cols-3 gap-2">
<div>
<label class="text-gray-300 text-sm">Hours</label>
<input id="hours" type="number" min="0" max="23" value="0" class="w-full bg-gray-800 text-white rounded p-2">
</div>
<div>
<label class="text-gray-300 text-sm">Minutes</label>
<input id="minutes" type="number" min="0" max="59" value="0" class="w-full bg-gray-800 text-white rounded p-2">
</div>
<div>
<label class="text-gray-300 text-sm">Seconds</label>
<input id="seconds" type="number" min="0" max="59" value="10" class="w-full bg-gray-800 text-white rounded p-2">
</div>
</div>
</div>
<div class="flex space-x-4 mb-4">
<button id="start-btn" class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition">
Start
</button>
<button id="pause-btn" class="flex-1 bg-yellow-600 hover:bg-yellow-700 text-white py-2 px-4 rounded transition" disabled>
Pause
</button>
<button id="reset-btn" class="flex-1 bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition">
Reset
</button>
</div>
</div>
<!-- History Section -->
<div class="mt-8 w-full max-w-md">
<h2 class="text-white text-xl font-bold mb-4">Timer History</h2>
<div id="history-list" class="bg-gray-900 rounded-lg p-4 max-h-64 overflow-y-auto">
<!-- History items will be added here dynamically -->
</div>
</div>
<!-- Analytics Section -->
<div class="mt-8 w-full max-w-md">
<h2 class="text-white text-xl font-bold mb-4">Time Analytics</h2>
<div class="bg-gray-900 rounded-lg p-4">
<div class="mb-4">
<label class="block text-white mb-2">Filter by Tag</label>
<select id="analytics-tag-filter" class="w-full bg-gray-800 text-white rounded p-2">
<option value="all">All Tags</option>
<option value="work">Work</option>
<option value="study">Study</option>
<option value="exercise">Exercise</option>
<option value="break">Break</option>
<option value="meeting">Meeting</option>
<option value="creative">Creative</option>
<option value="other">Other</option>
</select>
</div>
<div id="analytics-results" class="text-white">
<div class="flex justify-between mb-2">
<span>Total Time Tracked:</span>
<span id="total-time">00:00:00</span>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="bg-gray-800 p-3 rounded">
<div class="text-gray-400 text-sm">Today</div>
<div id="today-time">00:00:00</div>
</div>
<div class="bg-gray-800 p-3 rounded">
<div class="text-gray-400 text-sm">This Week</div>
<div id="week-time">00:00:00</div>
</div>
</div>
<div class="bg-gray-800 p-4 rounded">
<h3 class="text-gray-400 text-sm mb-2">Time Distribution</h3>
<canvas id="time-chart" height="200"></canvas>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const timerDisplay = document.getElementById('timer-display');
const progressBar = document.getElementById('progress-bar');
const hoursInput = document.getElementById('hours');
const minutesInput = document.getElementById('minutes');
const secondsInput = document.getElementById('seconds');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
let totalSeconds = 0;
let remainingSeconds = 0;
let timerInterval;
let isPaused = false;
let animationDuration = 0;
// Format time to HH:MM:SS
function formatTime(seconds) {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// Update the timer display
function updateDisplay() {
timerDisplay.textContent = formatTime(remainingSeconds);
}
// Start the timer
function startTimer() {
const hours = parseInt(hoursInput.value) || 0;
const minutes = parseInt(minutesInput.value) || 0;
const seconds = parseInt(secondsInput.value) || 0;
totalSeconds = hours * 3600 + minutes * 60 + seconds;
remainingSeconds = totalSeconds;
if (totalSeconds <= 0) {
alert('Please set a valid time!');
return;
}
// Calculate animation duration based on total time
animationDuration = totalSeconds;
// Reset progress bar
progressBar.style.animation = 'none';
progressBar.offsetHeight; // Trigger reflow
progressBar.style.animation = `progress ${animationDuration}s linear forwards`;
updateDisplay();
timerInterval = setInterval(() => {
if (!isPaused) {
remainingSeconds--;
updateDisplay();
if (remainingSeconds <= 0) {
clearInterval(timerInterval);
startBtn.disabled = false;
pauseBtn.disabled = true;
// Reset timer automatically
resetTimer();
saveToHistory();
// Play sound after reset completes
const audio = new Audio('https://assets.mixkit.co/sfx/preview/mixkit-alarm-digital-clock-beep-989.mp3');
audio.play();
}
}
}, 1000);
startBtn.disabled = true;
pauseBtn.disabled = false;
isPaused = false;
}
// Pause the timer
function pauseTimer() {
isPaused = !isPaused;
if (isPaused) {
pauseBtn.textContent = 'Resume';
progressBar.style.animationPlayState = 'paused';
} else {
pauseBtn.textContent = 'Pause';
progressBar.style.animationPlayState = 'running';
}
}
// Reset the timer
function resetTimer() {
clearInterval(timerInterval);
progressBar.style.animation = 'none';
remainingSeconds = 0;
updateDisplay();
startBtn.disabled = false;
pauseBtn.disabled = true;
pauseBtn.textContent = 'Pause';
isPaused = false;
}
// Save timer to history
function saveToHistory() {
const tagsInput = document.getElementById('tags-input');
const tag = tagsInput.value;
const duration = totalSeconds;
const date = new Date().toLocaleString();
if (totalSeconds > 0) {
const historyItem = document.createElement('div');
historyItem.className = 'mb-2 pb-2 border-b border-gray-700';
historyItem.innerHTML = `
<div class="text-white">${tag}</div>
<div class="flex justify-between text-gray-400 text-sm">
<span>${formatTime(duration)}</span>
<span>${date}</span>
</div>
`;
document.getElementById('history-list').prepend(historyItem);
// Add to analytics
addToAnalytics(tag, duration);
}
}
// Event listeners
startBtn.addEventListener('click', startTimer);
pauseBtn.addEventListener('click', pauseTimer);
resetBtn.addEventListener('click', () => {
resetTimer();
if (remainingSeconds <= 0 && totalSeconds > 0) {
saveToHistory();
}
});
// Initialize display
updateDisplay();
// Analytics data storage
let analyticsData = [];
// Load any existing history from localStorage
function loadHistory() {
const savedData = localStorage.getItem('timerAnalytics');
if (savedData) {
analyticsData = JSON.parse(savedData);
updateAnalytics();
}
}
// Save analytics data to localStorage
function saveAnalyticsData() {
localStorage.setItem('timerAnalytics', JSON.stringify(analyticsData));
}
// Update analytics display
function updateAnalytics() {
const filter = document.getElementById('analytics-tag-filter').value;
const filteredData = filter === 'all' ? analyticsData :
analyticsData.filter(item => item.tag === filter);
// Calculate total time
const totalSeconds = filteredData.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('total-time').textContent = formatTime(totalSeconds);
// Calculate today's time
const today = new Date().toLocaleDateString();
const todaySeconds = filteredData
.filter(item => new Date(item.date).toLocaleDateString() === today)
.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('today-time').textContent = formatTime(todaySeconds);
// Calculate this week's time
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const weekSeconds = filteredData
.filter(item => new Date(item.date) > oneWeekAgo)
.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('week-time').textContent = formatTime(weekSeconds);
}
// Add data to analytics
function addToAnalytics(tag, duration) {
analyticsData.push({
tag,
duration,
date: new Date().toISOString()
});
saveAnalyticsData();
updateAnalytics();
}
// Event listener for analytics filter
document.getElementById('analytics-tag-filter').addEventListener('change', updateAnalytics);
loadHistory();
loadTags();
// Tag management functionality
const tagModal = document.getElementById('tag-modal');
const manageTagsBtn = document.getElementById('manage-tags-btn');
const closeTagModal = document.getElementById('close-tag-modal');
const addTagBtn = document.getElementById('add-tag-btn');
const newTagInput = document.getElementById('new-tag-input');
const tagsList = document.getElementById('tags-list');
let userTags = [
{name: 'work', description: 'Work related tasks'},
{name: 'study', description: 'Learning and studying'},
{name: 'exercise', description: 'Physical activity'},
{name: 'break', description: 'Rest periods'},
{name: 'meeting', description: 'Meetings and calls'},
{name: 'creative', description: 'Creative work'},
{name: 'other', description: 'Other activities'}
];
// Load tags from localStorage
function loadTags() {
const savedTags = localStorage.getItem('userTags');
if (savedTags) {
userTags = JSON.parse(savedTags);
}
updateTagDropdown();
}
// Save tags to localStorage
function saveTags() {
localStorage.setItem('userTags', JSON.stringify(userTags));
updateTagDropdown();
updateAnalytics();
}
// Update tag dropdown
function updateTagDropdown() {
const tagsInput = document.getElementById('tags-input');
const analyticsFilter = document.getElementById('analytics-tag-filter');
// Clear existing options
tagsInput.innerHTML = '';
analyticsFilter.innerHTML = '<option value="all">All Tags</option>';
// Sort tags alphabetically
const sortedTags = [...userTags].sort((a, b) => a.name.localeCompare(b.name));
// Add new options
sortedTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.name;
option.textContent = tag.name.charAt(0).toUpperCase() + tag.name.slice(1);
option.title = tag.description || '';
tagsInput.appendChild(option);
const filterOption = document.createElement('option');
filterOption.value = tag.name;
filterOption.textContent = tag.name.charAt(0).toUpperCase() + tag.name.slice(1);
filterOption.title = tag.description || '';
analyticsFilter.appendChild(filterOption);
});
// Select the first tag by default
if (sortedTags.length > 0) {
tagsInput.value = sortedTags[0].name;
}
}
// Update tags list in modal
function updateTagsList() {
tagsList.innerHTML = '';
userTags.forEach((tag, index) => {
const tagItem = document.createElement('div');
tagItem.className = 'mb-2 p-2 bg-gray-700 rounded';
tagItem.innerHTML = `
<div class="flex items-center justify-between mb-1">
<span class="text-white font-medium">${tag.name}</span>
<button data-index="${index}" class="delete-tag text-red-400 hover:text-red-300">
Delete
</button>
</div>
<div class="text-gray-400 text-sm">${tag.description || 'No description'}</div>
`;
tagsList.appendChild(tagItem);
});
// Add event listeners to delete buttons
document.querySelectorAll('.delete-tag').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = e.target.getAttribute('data-index');
if (userTags.length > 1) {
userTags.splice(index, 1);
saveTags();
updateTagsList();
} else {
alert('You must have at least one tag!');
}
});
});
}
// Event listeners for tag management
manageTagsBtn.addEventListener('click', () => {
tagModal.classList.remove('hidden');
updateTagsList();
});
closeTagModal.addEventListener('click', () => {
tagModal.classList.add('hidden');
});
// Handle Enter key press in tag input
newTagInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTagBtn.click();
}
});
// Handle Enter key press in description
document.getElementById('tag-description').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTagBtn.click();
}
});
addTagBtn.addEventListener('click', () => {
const newTag = newTagInput.value.trim().toLowerCase();
const description = document.getElementById('tag-description').value.trim();
if (!newTag) {
alert('Please enter a tag name');
newTagInput.focus();
return;
}
const tagExists = userTags.some(t => t.name === newTag);
if (tagExists) {
alert('This tag already exists');
return;
}
userTags.push({
name: newTag,
description: description
});
saveTags();
updateTagsList();
newTagInput.value = '';
document.getElementById('tag-description').value = '';
newTagInput.focus();
});
// Initialize time chart
let timeChart = null;
function renderTimeChart(filteredData) {
const ctx = document.getElementById('time-chart').getContext('2d');
// Group by tag and sum durations
const tagData = {};
filteredData.forEach(item => {
if (!tagData[item.tag]) {
tagData[item.tag] = 0;
}
tagData[item.tag] += item.duration;
});
const labels = Object.keys(tagData);
const data = Object.values(tagData);
// Convert seconds to hours for better readability
const hoursData = data.map(seconds => (seconds / 3600).toFixed(2));
// Generate colors for each tag
const backgroundColors = labels.map((_, i) => {
const hue = (i * 360 / labels.length) % 360;
return `hsl(${hue}, 70%, 60%)`;
});
if (timeChart) {
timeChart.destroy();
}
timeChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: hoursData,
backgroundColor: backgroundColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#fff'
}
},
tooltip: {
callbacks: {
label: function(context) {
const hours = context.raw;
const minutes = (hours % 1) * 60;
return `${context.label}: ${Math.floor(hours)}h ${Math.round(minutes)}m`;
}
}
}
}
}
});
}
// Update analytics to include chart
function updateAnalytics() {
const filter = document.getElementById('analytics-tag-filter').value;
const filteredData = filter === 'all' ? analyticsData :
analyticsData.filter(item => item.tag === filter);
// Calculate total time
const totalSeconds = filteredData.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('total-time').textContent = formatTime(totalSeconds);
// Calculate today's time
const today = new Date().toLocaleDateString();
const todaySeconds = filteredData
.filter(item => new Date(item.date).toLocaleDateString() === today)
.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('today-time').textContent = formatTime(todaySeconds);
// Calculate this week's time
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const weekSeconds = filteredData
.filter(item => new Date(item.date) > oneWeekAgo)
.reduce((sum, item) => sum + item.duration, 0);
document.getElementById('week-time').textContent = formatTime(weekSeconds);
// Render chart if there's data
if (filteredData.length > 0) {
renderTimeChart(filteredData);
} else if (timeChart) {
timeChart.destroy();
timeChart = null;
}
}
// Fullscreen functionality
const fullscreenBtn = document.getElementById('fullscreen-btn');
const timerContainer = document.querySelector('.timer-container');
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
timerContainer.requestFullscreen().catch(err => {
alert(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
});
// Change fullscreen icon when in fullscreen
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
fullscreenBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 20L10 16m0 0v4m0-4H6m12 4l-4-4m4 4v-4m0 4h-4M4 4l4 4m0 0V4m0 4H4m16 0l-4 4m4-4V4m0 4h-4" />
</svg>
`;
} else {
fullscreenBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
`;
}
});
});
</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=suhas0/focus-flow-timer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>