|
|
<!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> |