Spaces:
Running
Running
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 | <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> |