| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Modern Todo App</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> |
| | |
| | .custom-scrollbar::-webkit-scrollbar { |
| | width: 6px; |
| | } |
| | .custom-scrollbar::-webkit-scrollbar-track { |
| | background: #f1f1f1; |
| | border-radius: 10px; |
| | } |
| | .custom-scrollbar::-webkit-scrollbar-thumb { |
| | background: #888; |
| | border-radius: 10px; |
| | } |
| | .custom-scrollbar::-webkit-scrollbar-thumb:hover { |
| | background: #555; |
| | } |
| | |
| | |
| | .task-checkbox:checked ~ .task-text { |
| | text-decoration: line-through; |
| | color: #9ca3af; |
| | } |
| | |
| | |
| | @keyframes pulse { |
| | 0% { transform: scale(1); } |
| | 50% { transform: scale(1.1); } |
| | 100% { transform: scale(1); } |
| | } |
| | .pulse-animation { |
| | animation: pulse 2s infinite; |
| | } |
| | |
| | |
| | input[type="datetime-local"]::-webkit-calendar-picker-indicator { |
| | filter: invert(0.5); |
| | cursor: pointer; |
| | } |
| | input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { |
| | filter: invert(0.3); |
| | } |
| | |
| | |
| | @keyframes fadeIn { |
| | from { opacity: 0; transform: translateY(10px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | .task-item { |
| | animation: fadeIn 0.3s ease-out forwards; |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gradient-to-br from-indigo-50 to-purple-50 min-h-screen"> |
| | <div class="container mx-auto px-4 py-8 max-w-3xl"> |
| | |
| | <header class="flex flex-col items-center mb-8"> |
| | <h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600 mb-2"> |
| | TaskFlow |
| | </h1> |
| | <p class="text-gray-500">Organize your day, one task at a time</p> |
| | |
| | |
| | <div class="flex gap-4 mt-4"> |
| | <div class="bg-white rounded-lg shadow p-3 text-center min-w-[100px]"> |
| | <div class="text-sm text-gray-500">Total</div> |
| | <div id="total-count" class="text-xl font-bold text-indigo-600">0</div> |
| | </div> |
| | <div class="bg-white rounded-lg shadow p-3 text-center min-w-[100px]"> |
| | <div class="text-sm text-gray-500">Completed</div> |
| | <div id="completed-count" class="text-xl font-bold text-green-500">0</div> |
| | </div> |
| | <div class="bg-white rounded-lg shadow p-3 text-center min-w-[100px]"> |
| | <div class="text-sm text-gray-500">Pending</div> |
| | <div id="pending-count" class="text-xl font-bold text-yellow-500">0</div> |
| | </div> |
| | </div> |
| | </header> |
| | |
| | |
| | <main class="bg-white rounded-xl shadow-lg overflow-hidden"> |
| | |
| | <div class="p-4 border-b border-gray-100"> |
| | <div class="flex gap-2"> |
| | <input |
| | type="text" |
| | id="task-input" |
| | placeholder="What needs to be done?" |
| | class="flex-1 px-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" |
| | > |
| | <input |
| | type="datetime-local" |
| | id="task-due" |
| | class="px-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" |
| | > |
| | <button |
| | id="add-task-btn" |
| | class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg transition-all duration-200 flex items-center gap-2" |
| | > |
| | <i class="fas fa-plus"></i> |
| | <span class="hidden sm:inline">Add Task</span> |
| | </button> |
| | </div> |
| | |
| | |
| | <div class="flex justify-center mt-4 gap-2"> |
| | <button data-filter="all" class="filter-btn active px-3 py-1 rounded-full text-sm bg-indigo-100 text-indigo-700">All</button> |
| | <button data-filter="active" class="filter-btn px-3 py-1 rounded-full text-sm hover:bg-gray-100">Active</button> |
| | <button data-filter="completed" class="filter-btn px-3 py-1 rounded-full text-sm hover:bg-gray-100">Completed</button> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="task-list" class="max-h-[400px] overflow-y-auto custom-scrollbar"> |
| | |
| | <div class="p-4 text-center text-gray-500" id="empty-state"> |
| | <i class="fas fa-tasks text-4xl mb-2 text-gray-300"></i> |
| | <p>No tasks yet. Add your first task!</p> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="p-3 border-t border-gray-100 flex justify-end"> |
| | <button id="clear-completed" class="text-sm text-gray-500 hover:text-red-500 flex items-center gap-1"> |
| | <i class="fas fa-trash-alt"></i> |
| | <span>Clear Completed</span> |
| | </button> |
| | </div> |
| | </main> |
| | |
| | |
| | <button id="scroll-to-top" class="fixed bottom-6 right-6 bg-indigo-600 text-white w-12 h-12 rounded-full shadow-lg flex items-center justify-center hover:bg-indigo-700 transition-all duration-200 hidden"> |
| | <i class="fas fa-arrow-up"></i> |
| | </button> |
| | </div> |
| |
|
| | <script> |
| | document.addEventListener('DOMContentLoaded', function() { |
| | |
| | const taskInput = document.getElementById('task-input'); |
| | const addTaskBtn = document.getElementById('add-task-btn'); |
| | const taskList = document.getElementById('task-list'); |
| | const emptyState = document.getElementById('empty-state'); |
| | const filterButtons = document.querySelectorAll('.filter-btn'); |
| | const clearCompletedBtn = document.getElementById('clear-completed'); |
| | const scrollToTopBtn = document.getElementById('scroll-to-top'); |
| | const totalCount = document.getElementById('total-count'); |
| | const completedCount = document.getElementById('completed-count'); |
| | const pendingCount = document.getElementById('pending-count'); |
| | |
| | |
| | let tasks = JSON.parse(localStorage.getItem('tasks')) || []; |
| | let currentFilter = 'all'; |
| | |
| | |
| | renderTasks(); |
| | updateStats(); |
| | |
| | |
| | addTaskBtn.addEventListener('click', addTask); |
| | taskInput.addEventListener('keypress', function(e) { |
| | if (e.key === 'Enter') addTask(); |
| | }); |
| | |
| | clearCompletedBtn.addEventListener('click', clearCompletedTasks); |
| | scrollToTopBtn.addEventListener('click', scrollToTop); |
| | |
| | |
| | filterButtons.forEach(btn => { |
| | btn.addEventListener('click', function() { |
| | filterButtons.forEach(b => b.classList.remove('active', 'bg-indigo-100', 'text-indigo-700')); |
| | this.classList.add('active', 'bg-indigo-100', 'text-indigo-700'); |
| | currentFilter = this.dataset.filter; |
| | renderTasks(); |
| | }); |
| | }); |
| | |
| | |
| | window.addEventListener('scroll', function() { |
| | if (window.pageYOffset > 300) { |
| | scrollToTopBtn.classList.remove('hidden'); |
| | } else { |
| | scrollToTopBtn.classList.add('hidden'); |
| | } |
| | }); |
| | |
| | |
| | function addTask() { |
| | const text = taskInput.value.trim(); |
| | if (text === '') return; |
| | |
| | const dueDate = document.getElementById('task-due').value; |
| | const newTask = { |
| | id: Date.now(), |
| | text: text, |
| | completed: false, |
| | createdAt: new Date().toISOString(), |
| | dueDate: dueDate || null |
| | }; |
| | |
| | tasks.unshift(newTask); |
| | saveTasks(); |
| | renderTasks(); |
| | updateStats(); |
| | |
| | taskInput.value = ''; |
| | taskInput.focus(); |
| | |
| | |
| | addTaskBtn.classList.add('pulse-animation'); |
| | setTimeout(() => { |
| | addTaskBtn.classList.remove('pulse-animation'); |
| | }, 2000); |
| | } |
| | |
| | function renderTasks() { |
| | |
| | let filteredTasks = []; |
| | |
| | switch (currentFilter) { |
| | case 'active': |
| | filteredTasks = tasks.filter(task => !task.completed); |
| | break; |
| | case 'completed': |
| | filteredTasks = tasks.filter(task => task.completed); |
| | break; |
| | default: |
| | filteredTasks = [...tasks]; |
| | } |
| | |
| | if (filteredTasks.length === 0) { |
| | emptyState.classList.remove('hidden'); |
| | taskList.innerHTML = ''; |
| | taskList.appendChild(emptyState); |
| | } else { |
| | emptyState.classList.add('hidden'); |
| | taskList.innerHTML = ''; |
| | |
| | filteredTasks.forEach(task => { |
| | const taskElement = createTaskElement(task); |
| | taskList.appendChild(taskElement); |
| | }); |
| | } |
| | } |
| | |
| | function createTaskElement(task) { |
| | const taskElement = document.createElement('div'); |
| | taskElement.className = `task-item p-4 border-b border-gray-100 flex items-center gap-3 hover:bg-gray-50 transition-colors duration-150`; |
| | taskElement.dataset.id = task.id; |
| | |
| | const checkbox = document.createElement('input'); |
| | checkbox.type = 'checkbox'; |
| | checkbox.className = 'task-checkbox w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer'; |
| | checkbox.checked = task.completed; |
| | checkbox.addEventListener('change', function() { |
| | toggleTaskCompletion(task.id); |
| | }); |
| | |
| | const taskText = document.createElement('span'); |
| | taskText.className = `task-text flex-1 ${task.completed ? 'line-through text-gray-400' : 'text-gray-700'}`; |
| | taskText.textContent = task.text; |
| | |
| | const dateSpan = document.createElement('span'); |
| | dateSpan.className = 'text-xs text-gray-400 ml-2'; |
| | const date = new Date(task.createdAt); |
| | dateSpan.textContent = date.toLocaleDateString(); |
| | |
| | if (task.dueDate) { |
| | const dueDate = new Date(task.dueDate); |
| | const dueSpan = document.createElement('span'); |
| | dueSpan.className = 'text-xs ml-2 ' + |
| | (new Date() > dueDate && !task.completed ? 'text-red-500' : 'text-gray-500'); |
| | dueSpan.textContent = 'Due: ' + dueDate.toLocaleString(); |
| | taskText.appendChild(dueSpan); |
| | } |
| | |
| | const deleteBtn = document.createElement('button'); |
| | deleteBtn.className = 'text-gray-400 hover:text-red-500 transition-colors duration-150'; |
| | deleteBtn.innerHTML = '<i class="fas fa-times"></i>'; |
| | deleteBtn.addEventListener('click', function(e) { |
| | e.stopPropagation(); |
| | deleteTask(task.id); |
| | }); |
| | |
| | taskText.appendChild(dateSpan); |
| | taskElement.appendChild(checkbox); |
| | taskElement.appendChild(taskText); |
| | taskElement.appendChild(deleteBtn); |
| | |
| | return taskElement; |
| | } |
| | |
| | function toggleTaskCompletion(taskId) { |
| | const taskIndex = tasks.findIndex(task => task.id === taskId); |
| | if (taskIndex !== -1) { |
| | tasks[taskIndex].completed = !tasks[taskIndex].completed; |
| | saveTasks(); |
| | renderTasks(); |
| | updateStats(); |
| | } |
| | } |
| | |
| | function deleteTask(taskId) { |
| | tasks = tasks.filter(task => task.id !== taskId); |
| | saveTasks(); |
| | renderTasks(); |
| | updateStats(); |
| | } |
| | |
| | function clearCompletedTasks() { |
| | tasks = tasks.filter(task => !task.completed); |
| | saveTasks(); |
| | renderTasks(); |
| | updateStats(); |
| | |
| | |
| | const clearBtn = clearCompletedBtn; |
| | clearBtn.innerHTML = '<i class="fas fa-check"></i> <span>Cleared!</span>'; |
| | clearBtn.classList.add('text-green-500'); |
| | setTimeout(() => { |
| | clearBtn.innerHTML = '<i class="fas fa-trash-alt"></i> <span>Clear Completed</span>'; |
| | clearBtn.classList.remove('text-green-500'); |
| | }, 2000); |
| | } |
| | |
| | function saveTasks() { |
| | localStorage.setItem('tasks', JSON.stringify(tasks)); |
| | } |
| | |
| | function updateStats() { |
| | totalCount.textContent = tasks.length; |
| | const completedTasks = tasks.filter(task => task.completed).length; |
| | completedCount.textContent = completedTasks; |
| | pendingCount.textContent = tasks.length - completedTasks; |
| | } |
| | |
| | function scrollToTop() { |
| | window.scrollTo({ |
| | top: 0, |
| | behavior: 'smooth' |
| | }); |
| | } |
| | }); |
| | </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=jphermans/todo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |