todo-lists / index.html
igoraguiar's picture
A full featured task / todo list app. Local persistence. Due date, categories, priority, lists. Post-Neumorphism: depth with clarity. Light/Dark. - Initial Deployment
edabd86 verified
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskSphere | 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">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
light: '#6366f1',
dark: '#818cf8'
},
secondary: {
light: '#f43f5e',
dark: '#fb7185'
},
surface: {
light: '#f8fafc',
dark: '#1e293b'
},
card: {
light: '#ffffff',
dark: '#334155'
}
},
boxShadow: {
'neumorph-light': '8px 8px 15px #d1d5db, -8px -8px 15px #ffffff',
'neumorph-dark': '8px 8px 15px #0f172a, -8px -8px 15px #475569',
'inner-neumorph-light': 'inset 3px 3px 5px #d1d5db, inset -3px -3px 5px #ffffff',
'inner-neumorph-dark': 'inset 3px 3px 5px #0f172a, inset -3px -3px 5px #475569'
}
}
}
}
</script>
<style>
.priority-high { border-left-color: #ef4444; }
.priority-medium { border-left-color: #f59e0b; }
.priority-low { border-left-color: #10b981; }
.category-work { background-color: rgba(99, 102, 241, 0.1); }
.category-personal { background-color: rgba(16, 185, 129, 0.1); }
.category-shopping { background-color: rgba(236, 72, 153, 0.1); }
.category-health { background-color: rgba(244, 63, 94, 0.1); }
.category-other { background-color: rgba(156, 163, 175, 0.1); }
.task-checkbox:checked + .task-label {
text-decoration: line-through;
opacity: 0.7;
}
.date-picker {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</head>
<body class="bg-surface-light dark:bg-surface-dark min-h-screen transition-colors duration-300">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<header class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-800 dark:text-white">TaskSphere</h1>
<p class="text-gray-600 dark:text-gray-300">Your productivity companion</p>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="p-2 rounded-full bg-card-light dark:bg-card-dark shadow-neumorph-light dark:shadow-neumorph-dark hover:shadow-inner-neumorph-light dark:hover:shadow-inner-neumorph-dark transition-all duration-300">
<i class="fas fa-moon text-gray-700 dark:text-yellow-300"></i>
</button>
<div class="relative">
<button id="list-dropdown-btn" class="px-4 py-2 rounded-lg bg-card-light dark:bg-card-dark shadow-neumorph-light dark:shadow-neumorph-dark hover:shadow-inner-neumorph-light dark:hover:shadow-inner-neumorph-dark transition-all duration-300 flex items-center">
<span id="current-list" class="font-medium text-gray-700 dark:text-gray-200">My Tasks</span>
<i class="fas fa-chevron-down ml-2 text-sm text-gray-500 dark:text-gray-400"></i>
</button>
<div id="list-dropdown" class="absolute hidden right-0 mt-2 w-48 rounded-lg bg-card-light dark:bg-card-dark shadow-neumorph-light dark:shadow-neumorph-dark z-10 overflow-hidden">
<div id="list-container" class="max-h-60 overflow-y-auto scrollbar-hide">
<!-- Lists will be populated here -->
</div>
<div class="border-t border-gray-200 dark:border-gray-700 p-2">
<button id="add-list-btn" class="w-full text-left px-3 py-2 text-sm text-primary-light dark:text-primary-dark hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<i class="fas fa-plus mr-2"></i> New List
</button>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Sidebar -->
<div class="lg:col-span-1">
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark mb-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Filters</h2>
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Priority</h3>
<div class="space-y-2">
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="priority" value="high">
<span class="ml-2 text-gray-700 dark:text-gray-300">High</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="priority" value="medium">
<span class="ml-2 text-gray-700 dark:text-gray-300">Medium</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="priority" value="low">
<span class="ml-2 text-gray-700 dark:text-gray-300">Low</span>
</label>
</div>
</div>
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Categories</h3>
<div class="space-y-2">
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="category" value="work">
<span class="ml-2 text-gray-700 dark:text-gray-300">Work</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="category" value="personal">
<span class="ml-2 text-gray-700 dark:text-gray-300">Personal</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="category" value="shopping">
<span class="ml-2 text-gray-700 dark:text-gray-300">Shopping</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="category" value="health">
<span class="ml-2 text-gray-700 dark:text-gray-300">Health</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="category" value="other">
<span class="ml-2 text-gray-700 dark:text-gray-300">Other</span>
</label>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Status</h3>
<div class="space-y-2">
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="status" value="completed">
<span class="ml-2 text-gray-700 dark:text-gray-300">Completed</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="status" value="pending">
<span class="ml-2 text-gray-700 dark:text-gray-300">Pending</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox rounded text-primary-light dark:text-primary-dark" data-filter="status" value="overdue">
<span class="ml-2 text-gray-700 dark:text-gray-300">Overdue</span>
</label>
</div>
</div>
</div>
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Stats</h2>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm text-gray-600 dark:text-gray-400 mb-1">
<span>Tasks Completed</span>
<span id="completed-count">0</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div id="completed-bar" class="bg-primary-light dark:bg-primary-dark h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm text-gray-600 dark:text-gray-400 mb-1">
<span>Tasks Pending</span>
<span id="pending-count">0</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div id="pending-bar" class="bg-yellow-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm text-gray-600 dark:text-gray-400 mb-1">
<span>Tasks Overdue</span>
<span id="overdue-count">0</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div id="overdue-bar" class="bg-secondary-light dark:bg-secondary-dark h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Task List -->
<div class="lg:col-span-3">
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark mb-6">
<form id="task-form" class="flex flex-col sm:flex-row gap-4">
<div class="flex-grow">
<input type="text" id="task-input" placeholder="Add a new task..." class="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white">
</div>
<div class="flex gap-2">
<div class="relative">
<select id="priority-select" class="appearance-none px-3 py-3 pr-8 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
<i class="fas fa-chevron-down text-xs"></i>
</div>
</div>
<div class="relative">
<select id="category-select" class="appearance-none px-3 py-3 pr-8 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
<option value="work">Work</option>
<option value="personal">Personal</option>
<option value="shopping">Shopping</option>
<option value="health">Health</option>
<option value="other" selected>Other</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
<i class="fas fa-chevron-down text-xs"></i>
</div>
</div>
<div class="relative">
<input type="date" id="due-date" class="date-picker px-3 py-3 pr-8 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
<i class="far fa-calendar text-xs"></i>
</div>
</div>
<button type="submit" class="px-4 py-3 rounded-lg bg-primary-light dark:bg-primary-dark text-white hover:bg-opacity-90 transition-colors duration-300">
<i class="fas fa-plus"></i>
</button>
</div>
</form>
</div>
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Tasks</h2>
<div class="flex items-center space-x-2">
<button id="clear-completed" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white transition-colors duration-300">
Clear Completed
</button>
<button id="sort-tasks" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white transition-colors duration-300 flex items-center">
<span>Sort By</span>
<i class="fas fa-chevron-down ml-1 text-xs"></i>
</button>
<div id="sort-dropdown" class="hidden absolute right-6 mt-8 w-40 rounded-lg bg-card-light dark:bg-card-dark shadow-neumorph-light dark:shadow-neumorph-dark z-10 py-1">
<button data-sort="due-date" class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Due Date</button>
<button data-sort="priority" class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Priority</button>
<button data-sort="category" class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Category</button>
<button data-sort="date-added" class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Date Added</button>
</div>
</div>
</div>
<div id="task-container" class="space-y-3">
<!-- Tasks will be populated here -->
<div class="text-center py-10 text-gray-500 dark:text-gray-400" id="empty-state">
<i class="fas fa-tasks text-4xl mb-3"></i>
<p class="text-lg">No tasks found</p>
<p class="text-sm">Add a new task to get started</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add List Modal -->
<div id="add-list-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800 dark:text-white">Create New List</h3>
<button id="close-list-modal" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form id="add-list-form">
<div class="mb-4">
<label for="list-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">List Name</label>
<input type="text" id="list-name" class="w-full px-4 py-2 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white">
</div>
<div class="flex justify-end space-x-3">
<button type="button" id="cancel-list" class="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-300">
Cancel
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary-light dark:bg-primary-dark text-white hover:bg-opacity-90 transition-colors duration-300">
Create List
</button>
</div>
</form>
</div>
</div>
<!-- Edit Task Modal -->
<div id="edit-task-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-card-light dark:bg-card-dark rounded-xl p-6 shadow-neumorph-light dark:shadow-neumorph-dark w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800 dark:text-white">Edit Task</h3>
<button id="close-edit-modal" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form id="edit-task-form">
<input type="hidden" id="edit-task-id">
<div class="mb-4">
<label for="edit-task-title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Task Title</label>
<input type="text" id="edit-task-title" class="w-full px-4 py-2 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white">
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="edit-priority-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Priority</label>
<select id="edit-priority-select" class="w-full px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<label for="edit-category-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Category</label>
<select id="edit-category-select" class="w-full px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
<option value="work">Work</option>
<option value="personal">Personal</option>
<option value="shopping">Shopping</option>
<option value="health">Health</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div class="mb-4">
<label for="edit-due-date" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Due Date</label>
<input type="date" id="edit-due-date" class="w-full px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-light dark:focus:ring-primary-dark focus:border-transparent text-gray-800 dark:text-white text-sm">
</div>
<div class="flex justify-end space-x-3">
<button type="button" id="delete-task" class="px-4 py-2 rounded-lg bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800 transition-colors duration-300">
Delete
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary-light dark:bg-primary-dark text-white hover:bg-opacity-90 transition-colors duration-300">
Save Changes
</button>
</div>
</form>
</div>
</div>
<script>
// DOM Elements
const themeToggle = document.getElementById('theme-toggle');
const taskForm = document.getElementById('task-form');
const taskInput = document.getElementById('task-input');
const prioritySelect = document.getElementById('priority-select');
const categorySelect = document.getElementById('category-select');
const dueDate = document.getElementById('due-date');
const taskContainer = document.getElementById('task-container');
const emptyState = document.getElementById('empty-state');
const clearCompletedBtn = document.getElementById('clear-completed');
const sortTasksBtn = document.getElementById('sort-tasks');
const sortDropdown = document.getElementById('sort-dropdown');
const listDropdownBtn = document.getElementById('list-dropdown-btn');
const listDropdown = document.getElementById('list-dropdown');
const currentList = document.getElementById('current-list');
const listContainer = document.getElementById('list-container');
const addListBtn = document.getElementById('add-list-btn');
const addListModal = document.getElementById('add-list-modal');
const closeListModal = document.getElementById('close-list-modal');
const cancelList = document.getElementById('cancel-list');
const addListForm = document.getElementById('add-list-form');
const listName = document.getElementById('list-name');
const editTaskModal = document.getElementById('edit-task-modal');
const closeEditModal = document.getElementById('close-edit-modal');
const editTaskForm = document.getElementById('edit-task-form');
const editTaskTitle = document.getElementById('edit-task-title');
const editPrioritySelect = document.getElementById('edit-priority-select');
const editCategorySelect = document.getElementById('edit-category-select');
const editDueDate = document.getElementById('edit-due-date');
const editTaskId = document.getElementById('edit-task-id');
const deleteTaskBtn = document.getElementById('delete-task');
const completedCount = document.getElementById('completed-count');
const pendingCount = document.getElementById('pending-count');
const overdueCount = document.getElementById('overdue-count');
const completedBar = document.getElementById('completed-bar');
const pendingBar = document.getElementById('pending-bar');
const overdueBar = document.getElementById('overdue-bar');
// Filter checkboxes
const filterCheckboxes = document.querySelectorAll('input[data-filter]');
// State
let tasks = [];
let lists = ['My Tasks', 'Work', 'Personal'];
let currentListName = 'My Tasks';
let currentSort = 'date-added';
let activeFilters = {
priority: [],
category: [],
status: []
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadFromLocalStorage();
renderLists();
renderTasks();
updateStats();
// Set default due date to today
const today = new Date().toISOString().split('T')[0];
dueDate.value = today;
dueDate.min = today;
});
// Theme Toggle
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
});
// Add Task
taskForm.addEventListener('submit', (e) => {
e.preventDefault();
if (taskInput.value.trim() === '') return;
const newTask = {
id: Date.now().toString(),
title: taskInput.value.trim(),
completed: false,
priority: prioritySelect.value,
category: categorySelect.value,
dueDate: dueDate.value,
list: currentListName,
createdAt: new Date().toISOString()
};
tasks.push(newTask);
saveToLocalStorage();
renderTasks();
updateStats();
// Reset form
taskInput.value = '';
prioritySelect.value = 'medium';
categorySelect.value = 'other';
dueDate.value = new Date().toISOString().split('T')[0];
});
// Toggle Task Completion
taskContainer.addEventListener('change', (e) => {
if (e.target.classList.contains('task-checkbox')) {
const taskId = e.target.dataset.id;
const task = tasks.find(task => task.id === taskId);
if (task) {
task.completed = e.target.checked;
saveToLocalStorage();
renderTasks();
updateStats();
}
}
});
// Edit Task
taskContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('edit-task') || e.target.parentElement.classList.contains('edit-task')) {
const taskId = e.target.closest('[data-id]').dataset.id;
const task = tasks.find(task => task.id === taskId);
if (task) {
editTaskId.value = task.id;
editTaskTitle.value = task.title;
editPrioritySelect.value = task.priority;
editCategorySelect.value = task.category;
editDueDate.value = task.dueDate;
editTaskModal.classList.remove('hidden');
}
}
});
// Save Edited Task
editTaskForm.addEventListener('submit', (e) => {
e.preventDefault();
const taskId = editTaskId.value;
const task = tasks.find(task => task.id === taskId);
if (task) {
task.title = editTaskTitle.value.trim();
task.priority = editPrioritySelect.value;
task.category = editCategorySelect.value;
task.dueDate = editDueDate.value;
saveToLocalStorage();
renderTasks();
updateStats();
editTaskModal.classList.add('hidden');
}
});
// Delete Task
deleteTaskBtn.addEventListener('click', () => {
const taskId = editTaskId.value;
tasks = tasks.filter(task => task.id !== taskId);
saveToLocalStorage();
renderTasks();
updateStats();
editTaskModal.classList.add('hidden');
});
// Clear Completed Tasks
clearCompletedBtn.addEventListener('click', () => {
tasks = tasks.filter(task => !task.completed || task.list !== currentListName);
saveToLocalStorage();
renderTasks();
updateStats();
});
// Sort Tasks
sortTasksBtn.addEventListener('click', () => {
sortDropdown.classList.toggle('hidden');
});
// Close sort dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!sortTasksBtn.contains(e.target) && !sortDropdown.contains(e.target)) {
sortDropdown.classList.add('hidden');
}
});
// Sort Tasks by selected option
sortDropdown.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
currentSort = e.target.dataset.sort;
renderTasks();
sortDropdown.classList.add('hidden');
}
});
// List Dropdown
listDropdownBtn.addEventListener('click', () => {
listDropdown.classList.toggle('hidden');
});
// Close list dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!listDropdownBtn.contains(e.target) && !listDropdown.contains(e.target)) {
listDropdown.classList.add('hidden');
}
});
// Change List
listContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('list-item') || e.target.parentElement.classList.contains('list-item')) {
const listName = e.target.closest('[data-list]').dataset.list;
currentListName = listName;
currentList.textContent = listName;
renderTasks();
updateStats();
listDropdown.classList.add('hidden');
}
});
// Add List Modal
addListBtn.addEventListener('click', () => {
addListModal.classList.remove('hidden');
listName.focus();
});
closeListModal.addEventListener('click', () => {
addListModal.classList.add('hidden');
});
cancelList.addEventListener('click', () => {
addListModal.classList.add('hidden');
});
// Close edit modal
closeEditModal.addEventListener('click', () => {
editTaskModal.classList.add('hidden');
});
// Close modals when clicking outside
window.addEventListener('click', (e) => {
if (e.target === addListModal) {
addListModal.classList.add('hidden');
}
if (e.target === editTaskModal) {
editTaskModal.classList.add('hidden');
}
});
// Add New List
addListForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = listName.value.trim();
if (name && !lists.includes(name)) {
lists.push(name);
currentListName = name;
currentList.textContent = name;
saveToLocalStorage();
renderLists();
renderTasks();
addListModal.classList.add('hidden');
listName.value = '';
}
});
// Filter Tasks
filterCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
const filterType = checkbox.dataset.filter;
const value = checkbox.value;
if (checkbox.checked) {
if (!activeFilters[filterType].includes(value)) {
activeFilters[filterType].push(value);
}
} else {
activeFilters[filterType] = activeFilters[filterType].filter(item => item !== value);
}
renderTasks();
});
});
// Render Tasks
function renderTasks() {
// Filter tasks by current list
let filteredTasks = tasks.filter(task => task.list === currentListName);
// Apply filters
if (activeFilters.priority.length > 0) {
filteredTasks = filteredTasks.filter(task => activeFilters.priority.includes(task.priority));
}
if (activeFilters.category.length > 0) {
filteredTasks = filteredTasks.filter(task => activeFilters.category.includes(task.category));
}
if (activeFilters.status.length > 0) {
filteredTasks = filteredTasks.filter(task => {
if (activeFilters.status.includes('completed') && task.completed) return true;
if (activeFilters.status.includes('pending') && !task.completed) {
const today = new Date().toISOString().split('T')[0];
return !task.dueDate || task.dueDate >= today;
}
if (activeFilters.status.includes('overdue') && !task.completed && task.dueDate) {
const today = new Date().toISOString().split('T')[0];
return task.dueDate < today;
}
return false;
});
}
// Sort tasks
filteredTasks.sort((a, b) => {
if (currentSort === 'due-date') {
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return new Date(a.dueDate) - new Date(b.dueDate);
} else if (currentSort === 'priority') {
const priorityOrder = { high: 1, medium: 2, low: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
} else if (currentSort === 'category') {
return a.category.localeCompare(b.category);
} else { // date-added
return new Date(b.createdAt) - new Date(a.createdAt);
}
});
// Clear task container
taskContainer.innerHTML = '';
// Show empty state if no tasks
if (filteredTasks.length === 0) {
taskContainer.appendChild(emptyState);
return;
}
// Group tasks by date (Today, Tomorrow, Upcoming, Overdue, No Date)
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().split('T')[0];
const todayTasks = filteredTasks.filter(task => task.dueDate === today && !task.completed);
const tomorrowTasks = filteredTasks.filter(task => task.dueDate === tomorrowStr && !task.completed);
const upcomingTasks = filteredTasks.filter(task => task.dueDate > tomorrowStr && !task.completed);
const overdueTasks = filteredTasks.filter(task => task.dueDate && task.dueDate < today && !task.completed);
const noDateTasks = filteredTasks.filter(task => !task.dueDate && !task.completed);
const completedTasks = filteredTasks.filter(task => task.completed);
// Render task groups
if (overdueTasks.length > 0) {
renderTaskGroup('Overdue', overdueTasks);
}
if (todayTasks.length > 0) {
renderTaskGroup('Today', todayTasks);
}
if (tomorrowTasks.length > 0) {
renderTaskGroup('Tomorrow', tomorrowTasks);
}
if (upcomingTasks.length > 0) {
renderTaskGroup('Upcoming', upcomingTasks);
}
if (noDateTasks.length > 0) {
renderTaskGroup('No Date', noDateTasks);
}
if (completedTasks.length > 0) {
renderTaskGroup('Completed', completedTasks, true);
}
}
function renderTaskGroup(title, tasks, isCompleted = false) {
const groupDiv = document.createElement('div');
groupDiv.className = 'mb-6';
const groupTitle = document.createElement('h3');
groupTitle.className = 'text-sm font-medium text-gray-500 dark:text-gray-400 mb-3 uppercase tracking-wider';
groupTitle.textContent = title;
groupDiv.appendChild(groupTitle);
const tasksList = document.createElement('div');
tasksList.className = 'space-y-2';
tasks.forEach(task => {
const taskElement = document.createElement('div');
taskElement.className = `bg-gray-50 dark:bg-gray-700 rounded-lg p-4 flex items-start border-l-4 ${isCompleted ? 'opacity-70' : ''} ${task.priority === 'high' ? 'priority-high' : task.priority === 'medium' ? 'priority-medium' : 'priority-low'}`;
taskElement.dataset.id = task.id;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'task-checkbox mt-1 h-4 w-4 rounded border-gray-300 text-primary-light dark:text-primary-dark focus:ring-primary-light dark:focus:ring-primary-dark';
checkbox.dataset.id = task.id;
checkbox.checked = task.completed;
const taskContent = document.createElement('div');
taskContent.className = 'ml-3 flex-1';
const taskTitle = document.createElement('label');
taskTitle.className = `task-label block text-gray-800 dark:text-gray-200 ${isCompleted ? 'line-through' : ''}`;
taskTitle.textContent = task.title;
taskTitle.htmlFor = `task-${task.id}`;
const taskMeta = document.createElement('div');
taskMeta.className = 'flex flex-wrap items-center mt-1 text-xs text-gray-500 dark:text-gray-400 space-x-3';
const categorySpan = document.createElement('span');
categorySpan.className = `px-2 py-1 rounded-full ${task.category}-${task.category} capitalize`;
categorySpan.textContent = task.category;
const dueDateSpan = document.createElement('span');
dueDateSpan.className = 'flex items-center';
if (task.dueDate) {
const dueDate = new Date(task.dueDate);
const today = new Date();
today.setHours(0, 0, 0, 0);
const dueDateStr = dueDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
if (dueDate < today && !task.completed) {
dueDateSpan.innerHTML = `<i class="fas fa-exclamation-circle text-red-500 mr-1"></i> ${dueDateStr}`;
} else {
dueDateSpan.innerHTML = `<i class="far fa-calendar-alt mr-1"></i> ${dueDateStr}`;
}
}
taskMeta.appendChild(categorySpan);
if (task.dueDate) taskMeta.appendChild(dueDateSpan);
taskContent.appendChild(taskTitle);
taskContent.appendChild(taskMeta);
const taskActions = document.createElement('div');
taskActions.className = 'ml-2 flex items-center space-x-2';
const editBtn = document.createElement('button');
editBtn.className = 'edit-task text-gray-400 hover:text-primary-light dark:hover:text-primary-dark transition-colors duration-300';
editBtn.innerHTML = '<i class="fas fa-pencil-alt"></i>';
editBtn.title = 'Edit task';
taskActions.appendChild(editBtn);
taskElement.appendChild(checkbox);
taskElement.appendChild(taskContent);
taskElement.appendChild(taskActions);
tasksList.appendChild(taskElement);
});
groupDiv.appendChild(tasksList);
taskContainer.appendChild(groupDiv);
}
// Render Lists
function renderLists() {
listContainer.innerHTML = '';
lists.forEach(list => {
const listItem = document.createElement('div');
listItem.className = `list-item px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center ${list === currentListName ? 'bg-gray-100 dark:bg-gray-700' : ''}`;
listItem.dataset.list = list;
const listIcon = document.createElement('i');
listIcon.className = 'far fa-list-alt mr-2';
const listName = document.createElement('span');
listName.textContent = list;
const taskCount = document.createElement('span');
taskCount.className = 'ml-auto text-xs bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-full px-2 py-1';
taskCount.textContent = tasks.filter(task => task.list === list).length;
listItem.appendChild(listIcon);
listItem.appendChild(listName);
listItem.appendChild(taskCount);
listContainer.appendChild(listItem);
});
}
// Update Stats
function updateStats() {
const currentTasks = tasks.filter(task => task.list === currentListName);
const totalTasks = currentTasks.length;
const completedTasks = currentTasks.filter(task => task.completed).length;
const pendingTasks = currentTasks.filter(task => !task.completed).length;
const today = new Date().toISOString().split('T')[0];
const overdueTasks = currentTasks.filter(task => !task.completed && task.dueDate && task.dueDate < today).length;
completedCount.textContent = completedTasks;
pendingCount.textContent = pendingTasks;
overdueCount.textContent = overdueTasks;
const completedPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
const pendingPercentage = totalTasks > 0 ? Math.round((pendingTasks / totalTasks) * 100) : 0;
const overduePercentage = totalTasks > 0 ? Math.round((overdueTasks / totalTasks) * 100) : 0;
completedBar.style.width = `${completedPercentage}%`;
pendingBar.style.width = `${pendingPercentage}%`;
overdueBar.style.width = `${overduePercentage}%`;
}
// Local Storage
function saveToLocalStorage() {
localStorage.setItem('tasks', JSON.stringify(tasks));
localStorage.setItem('lists', JSON.stringify(lists));
localStorage.setItem('currentList', currentListName);
}
function loadFromLocalStorage() {
const savedTasks = localStorage.getItem('tasks');
const savedLists = localStorage.getItem('lists');
const savedCurrentList = localStorage.getItem('currentList');
if (savedTasks) tasks = JSON.parse(savedTasks);
if (savedLists) lists = JSON.parse(savedLists);
if (savedCurrentList) currentListName = savedCurrentList;
// Set theme from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
</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=igoraguiar/todo-lists" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>