Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ZenFocus - Elegant 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 = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: { | |
| 50: '#f0f9ff', | |
| 100: '#e0f2fe', | |
| 200: '#bae6fd', | |
| 300: '#7dd3fc', | |
| 400: '#38bdf8', | |
| 500: '#0ea5e9', | |
| 600: '#0284c7', | |
| 700: '#0369a1', | |
| 800: '#075985', | |
| 900: '#0c4a6e', | |
| } | |
| }, | |
| fontFamily: { | |
| sans: ['"Inter"', 'system-ui', '-apple-system', 'sans-serif'], | |
| }, | |
| boxShadow: { | |
| 'soft': '0 4px 20px -2px rgba(0, 0, 0, 0.08)', | |
| 'inner-glow': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); | |
| min-height: 100vh; | |
| } | |
| .task-item { | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .task-item:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 8px -1px rgba(0, 0, 0, 0.05); | |
| } | |
| .task-item:hover .task-actions { | |
| opacity: 1; | |
| } | |
| .task-actions { | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .checkbox-custom { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid #e2e8f0; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| position: relative; | |
| transition: all 0.2s ease; | |
| } | |
| .checkbox-custom:checked { | |
| background-color: #0ea5e9; | |
| border-color: #0ea5e9; | |
| } | |
| .checkbox-custom:checked::after { | |
| content: ""; | |
| position: absolute; | |
| left: 5px; | |
| top: 1px; | |
| width: 4px; | |
| height: 8px; | |
| border: solid white; | |
| border-width: 0 2px 2px 0; | |
| transform: rotate(45deg); | |
| } | |
| .priority-high { | |
| border-left: 3px solid #ef4444; | |
| background-color: rgba(239, 68, 68, 0.03); | |
| } | |
| .priority-medium { | |
| border-left: 3px solid #f59e0b; | |
| background-color: rgba(245, 158, 11, 0.03); | |
| } | |
| .priority-low { | |
| border-left: 3px solid #10b981; | |
| background-color: rgba(16, 185, 129, 0.03); | |
| } | |
| /* Animation for new task */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(8px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .task-animate { | |
| animation: fadeIn 0.3s ease-out forwards; | |
| } | |
| /* Pulse animation for empty state */ | |
| @keyframes pulse { | |
| 0% { opacity: 0.8; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.8; } | |
| } | |
| .empty-pulse { | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| /* Custom scrollbar */ | |
| .task-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .task-list::-webkit-scrollbar-track { | |
| background: #f1f5f9; | |
| border-radius: 10px; | |
| } | |
| .task-list::-webkit-scrollbar-thumb { | |
| background: #cbd5e1; | |
| border-radius: 10px; | |
| } | |
| .task-list::-webkit-scrollbar-thumb:hover { | |
| background: #94a3b8; | |
| } | |
| /* Floating action button */ | |
| .fab { | |
| box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.15); | |
| transition: all 0.2s ease; | |
| } | |
| .fab:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 16px -2px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Gradient text */ | |
| .gradient-text { | |
| background: linear-gradient(90deg, #0ea5e9 0%, #3b82f6 100%); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| /* Ripple effect */ | |
| .ripple { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .ripple:after { | |
| content: ""; | |
| display: block; | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| background-image: radial-gradient(circle, #fff 10%, transparent 10.01%); | |
| background-repeat: no-repeat; | |
| background-position: 50%; | |
| transform: scale(10, 10); | |
| opacity: 0; | |
| transition: transform .5s, opacity 1s; | |
| } | |
| .ripple:active:after { | |
| transform: scale(0, 0); | |
| opacity: 0.2; | |
| transition: 0s; | |
| } | |
| /* Shimmer effect */ | |
| .shimmer { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .shimmer::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); | |
| animation: shimmer 1.5s infinite; | |
| } | |
| @keyframes shimmer { | |
| 100% { | |
| left: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="antialiased text-gray-800"> | |
| <div class="container mx-auto px-4 py-8 md:py-12 max-w-2xl"> | |
| <!-- Header --> | |
| <header class="mb-10"> | |
| <div class="text-center mb-6"> | |
| <div class="flex justify-center mb-3"> | |
| <div class="w-12 h-12 bg-primary-100 rounded-xl flex items-center justify-center shadow-soft"> | |
| <i class="fas fa-check-circle text-2xl gradient-text"></i> | |
| </div> | |
| </div> | |
| <h1 class="text-4xl font-extrabold gradient-text mb-1">ZenFocus</h1> | |
| <p class="text-gray-500 text-lg">Organize your day with simplicity</p> | |
| </div> | |
| <div class="relative w-full mb-1"> | |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
| <i class="fas fa-plus-circle text-gray-400"></i> | |
| </div> | |
| <input | |
| type="text" | |
| id="new-task-input" | |
| placeholder="Add a new task..." | |
| class="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent shadow-sm transition duration-200" | |
| autocomplete="off" | |
| > | |
| <div class="absolute inset-y-0 right-0 flex items-center pr-3"> | |
| <button | |
| id="add-task-btn" | |
| class="fab p-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-xl hover:shadow-md" | |
| > | |
| <i class="fas fa-paper-plane text-sm"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="text-xs text-gray-400 pl-3 mt-1">Tip: Press Enter to add quickly</p> | |
| </header> | |
| <!-- Stats Cards --> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4 mb-6"> | |
| <div class="bg-white p-4 rounded-xl shadow-soft transition hover:shadow-md"> | |
| <div class="flex items-center"> | |
| <div class="p-2 mr-3 rounded-lg bg-primary-50"> | |
| <i class="fas fa-tasks text-primary-500"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">Total Tasks</p> | |
| <h3 class="text-xl font-semibold" id="total-tasks">0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl shadow-soft transition hover:shadow-md"> | |
| <div class="flex items-center"> | |
| <div class="p-2 mr-3 rounded-lg bg-green-50"> | |
| <i class="fas fa-check-circle text-green-500"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">Completed</p> | |
| <h3 class="text-xl font-semibold" id="completed-tasks">0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl shadow-soft transition hover:shadow-md"> | |
| <div class="flex items-center"> | |
| <div class="p-2 mr-3 rounded-lg bg-amber-50"> | |
| <i class="fas fa-clock text-amber-500"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-500">Pending</p> | |
| <h3 class="text-xl font-semibold" id="pending-tasks">0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Filters --> | |
| <div class="flex flex-wrap justify-between items-center mb-6 bg-white p-4 rounded-xl shadow-soft"> | |
| <div class="flex space-x-1 mb-2 sm:mb-0"> | |
| <button | |
| id="filter-all" | |
| class="filter-btn active px-3 py-1.5 rounded-lg bg-primary-100 text-primary-800 font-medium text-sm ripple" | |
| > | |
| All | |
| </button> | |
| <button | |
| id="filter-active" | |
| class="filter-btn px-3 py-1.5 rounded-lg hover:bg-gray-50 text-gray-600 text-sm ripple" | |
| > | |
| Active | |
| </button> | |
| <button | |
| id="filter-completed" | |
| class="filter-btn px-3 py-1.5 rounded-lg hover:bg-gray-50 text-gray-600 text-sm ripple" | |
| > | |
| Completed | |
| </button> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="relative"> | |
| <select | |
| id="sort-tasks" | |
| class="appearance-none text-sm border border-gray-200 rounded-lg px-3 py-1.5 pr-8 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white" | |
| > | |
| <option value="date">Date Added</option> | |
| <option value="priority">Priority</option> | |
| <option value="name">Name</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500"> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Task List --> | |
| <div class="bg-white rounded-xl shadow-soft overflow-hidden"> | |
| <div id="task-list" class="task-list max-h-[480px] overflow-y-auto"> | |
| <!-- Empty state --> | |
| <div class="p-8 text-center" id="empty-state"> | |
| <div class="mb-4 empty-pulse"> | |
| <svg class="mx-auto h-16 w-16 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"> | |
| <path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> | |
| </svg> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-700 mb-1">No tasks yet</h3> | |
| <p class="text-gray-500">Get started by adding your first task</p> | |
| <div class="mt-4"> | |
| <button class="text-sm text-primary-600 hover:text-primary-800 flex items-center justify-center mx-auto" id="sample-task-btn"> | |
| <i class="fas fa-magic mr-1.5"></i> Add sample task | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer Actions --> | |
| <div class="border-t border-gray-100 p-4 flex justify-between items-center bg-gray-50 rounded-b-xl"> | |
| <div class="text-sm text-gray-600 font-medium" id="remaining-count">0 items left</div> | |
| <button | |
| id="clear-completed" | |
| class="text-sm text-primary-600 hover:text-primary-800 font-medium ripple" | |
| > | |
| <i class="fas fa-broom mr-1"></i> Clear completed | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Priority Modal --> | |
| <div id="priority-modal" class="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center hidden z-50 backdrop-blur-sm"> | |
| <div class="bg-white rounded-xl p-6 w-full max-w-sm mx-4 shadow-2xl transform transition-all" id="modal-content"> | |
| <h3 class="text-lg font-semibold text-gray-900 mb-4">Set Priority</h3> | |
| <div class="space-y-2"> | |
| <button | |
| class="priority-option w-full text-left px-4 py-3 rounded-lg border border-red-100 bg-red-50 hover:bg-red-100 transition-colors group ripple" | |
| data-priority="high" | |
| > | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-lg bg-red-100 flex items-center justify-center mr-3 group-hover:bg-red-200 transition-colors"> | |
| <i class="fas fa-exclamation-circle text-red-600 text-sm"></i> | |
| </div> | |
| <div> | |
| <div class="font-medium text-red-800">High Priority</div> | |
| <div class="text-xs text-red-600">Urgent and important</div> | |
| </div> | |
| </div> | |
| </button> | |
| <button | |
| class="priority-option w-full text-left px-4 py-3 rounded-lg border border-amber-100 bg-amber-50 hover:bg-amber-100 transition-colors group ripple" | |
| data-priority="medium" | |
| > | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center mr-3 group-hover:bg-amber-200 transition-colors"> | |
| <i class="fas fa-exclamation text-amber-600 text-sm"></i> | |
| </div> | |
| <div> | |
| <div class="font-medium text-amber-800">Medium Priority</div> | |
| <div class="text-xs text-amber-600">Important but not urgent</div> | |
| </div> | |
| </div> | |
| </button> | |
| <button | |
| class="priority-option w-full text-left px-4 py-3 rounded-lg border border-green-100 bg-green-50 hover:bg-green-100 transition-colors group ripple" | |
| data-priority="low" | |
| > | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-lg bg-green-100 flex items-center justify-center mr-3 group-hover:bg-green-200 transition-colors"> | |
| <i class="fas fa-arrow-down text-green-600 text-sm"></i> | |
| </div> | |
| <div> | |
| <div class="font-medium text-green-800">Low Priority</div> | |
| <div class="text-xs text-green-600">Neither urgent nor important</div> | |
| </div> | |
| </div> | |
| </button> | |
| </div> | |
| <div class="mt-6 flex justify-end"> | |
| <button | |
| id="cancel-priority" | |
| class="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium ripple" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Confirmation Modal --> | |
| <div id="confirm-modal" class="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center hidden z-50 backdrop-blur-sm"> | |
| <div class="bg-white rounded-xl p-6 w-full max-w-sm mx-4 shadow-2xl"> | |
| <div class="text-center"> | |
| <div class="mx-auto h-12 w-12 rounded-full bg-red-100 flex items-center justify-center mb-4"> | |
| <i class="fas fa-exclamation text-red-600"></i> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-900 mb-2" id="confirm-title">Are you sure?</h3> | |
| <p class="text-sm text-gray-500 mb-4" id="confirm-message">This action cannot be undone.</p> | |
| <div class="flex justify-center space-x-3"> | |
| <button | |
| id="confirm-cancel" | |
| class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium ripple" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| id="confirm-ok" | |
| class="px-4 py-2 bg-red-600 rounded-lg text-white hover:bg-red-700 font-medium ripple" | |
| > | |
| Confirm | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const taskInput = document.getElementById('new-task-input'); | |
| const addTaskBtn = document.getElementById('add-task-btn'); | |
| const taskList = document.getElementById('task-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const filterAll = document.getElementById('filter-all'); | |
| const filterActive = document.getElementById('filter-active'); | |
| const filterCompleted = document.getElementById('filter-completed'); | |
| const clearCompleted = document.getElementById('clear-completed'); | |
| const remainingCount = document.getElementById('remaining-count'); | |
| const totalTasksEl = document.getElementById('total-tasks'); | |
| const completedTasksEl = document.getElementById('completed-tasks'); | |
| const pendingTasksEl = document.getElementById('pending-tasks'); | |
| const sortSelect = document.getElementById('sort-tasks'); | |
| const priorityModal = document.getElementById('priority-modal'); | |
| const priorityOptions = document.querySelectorAll('.priority-option'); | |
| const cancelPriority = document.getElementById('cancel-priority'); | |
| const sampleTaskBtn = document.getElementById('sample-task-btn'); | |
| const confirmModal = document.getElementById('confirm-modal'); | |
| const confirmTitle = document.getElementById('confirm-title'); | |
| const confirmMessage = document.getElementById('confirm-message'); | |
| const confirmCancel = document.getElementById('confirm-cancel'); | |
| const confirmOk = document.getElementById('confirm-ok'); | |
| // State | |
| let tasks = JSON.parse(localStorage.getItem('tasks')) || []; | |
| let currentFilter = 'all'; | |
| let currentSort = 'date'; | |
| let currentTaskIdForPriority = null; | |
| let confirmCallback = null; | |
| // Initialize | |
| updateTaskList(); | |
| updateStats(); | |
| // Event Listeners | |
| addTaskBtn.addEventListener('click', addTask); | |
| taskInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') addTask(); | |
| }); | |
| filterAll.addEventListener('click', () => setFilter('all')); | |
| filterActive.addEventListener('click', () => setFilter('active')); | |
| filterCompleted.addEventListener('click', () => setFilter('completed')); | |
| clearCompleted.addEventListener('click', () => { | |
| showConfirm( | |
| 'Clear completed tasks?', | |
| 'This will remove all completed tasks from your list.', | |
| clearCompletedTasks | |
| ); | |
| }); | |
| sortSelect.addEventListener('change', (e) => { | |
| currentSort = e.target.value; | |
| updateTaskList(); | |
| }); | |
| cancelPriority.addEventListener('click', () => { | |
| priorityModal.classList.add('hidden'); | |
| }); | |
| priorityOptions.forEach(option => { | |
| option.addEventListener('click', function() { | |
| const priority = this.getAttribute('data-priority'); | |
| setTaskPriority(currentTaskIdForPriority, priority); | |
| priorityModal.classList.add('hidden'); | |
| }); | |
| }); | |
| sampleTaskBtn.addEventListener('click', addSampleTasks); | |
| confirmCancel.addEventListener('click', () => { | |
| confirmModal.classList.add('hidden'); | |
| }); | |
| confirmOk.addEventListener('click', () => { | |
| if (confirmCallback) confirmCallback(); | |
| confirmModal.classList.add('hidden'); | |
| }); | |
| // Functions | |
| function addTask() { | |
| const taskText = taskInput.value.trim(); | |
| if (taskText === '') return; | |
| const newTask = { | |
| id: Date.now(), | |
| text: taskText, | |
| completed: false, | |
| priority: 'medium', // default priority | |
| createdAt: new Date().toISOString() | |
| }; | |
| tasks.unshift(newTask); | |
| saveTasks(); | |
| taskInput.value = ''; | |
| updateTaskList(); | |
| updateStats(); | |
| taskInput.focus(); | |
| // Animate the add button | |
| addTaskBtn.classList.add('animate-ping'); | |
| setTimeout(() => { | |
| addTaskBtn.classList.remove('animate-ping'); | |
| }, 300); | |
| // Show priority modal for new task after a slight delay | |
| setTimeout(() => { | |
| currentTaskIdForPriority = newTask.id; | |
| showPriorityModal(); | |
| }, 50); | |
| } | |
| function addSampleTasks() { | |
| const sampleTasks = [ | |
| { text: "Complete project presentation", priority: "high" }, | |
| { text: "Reply to important emails", priority: "high" }, | |
| { text: "Buy groceries for the week", priority: "medium" }, | |
| { text: "Schedule dentist appointment", priority: "medium" }, | |
| { text: "Read 20 pages of book", priority: "low" }, | |
| { text: "Organize workspace", priority: "low" } | |
| ]; | |
| sampleTasks.forEach(task => { | |
| const newTask = { | |
| id: Date.now() + Math.random(), | |
| text: task.text, | |
| completed: false, | |
| priority: task.priority, | |
| createdAt: new Date().toISOString() | |
| }; | |
| tasks.unshift(newTask); | |
| }); | |
| saveTasks(); | |
| updateTaskList(); | |
| updateStats(); | |
| setFilter('all'); | |
| // Show shimmer effect on empty state before it disappears | |
| emptyState.classList.add('shimmer'); | |
| setTimeout(() => { | |
| emptyState.classList.remove('shimmer'); | |
| }, 300); | |
| } | |
| function setTaskPriority(taskId, priority) { | |
| const taskIndex = tasks.findIndex(task => task.id === taskId); | |
| if (taskIndex !== -1) { | |
| tasks[taskIndex].priority = priority; | |
| saveTasks(); | |
| updateTaskList(); | |
| // Show priority indicator | |
| const priorityEl = document.querySelector(`.priority-btn[data-id="${taskId}"]`); | |
| if (priorityEl) { | |
| priorityEl.classList.add('animate-bounce'); | |
| setTimeout(() => { | |
| priorityEl.classList.remove('animate-bounce'); | |
| }, 1000); | |
| } | |
| } | |
| } | |
| function toggleTaskComplete(taskId) { | |
| const task = tasks.find(task => task.id === taskId); | |
| if (task) { | |
| task.completed = !task.completed; | |
| saveTasks(); | |
| updateTaskList(); | |
| updateStats(); | |
| // Play completion sound if completed | |
| if (task.completed) { | |
| playCompletionSound(); | |
| } | |
| } | |
| } | |
| function playCompletionSound() { | |
| const audio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU' + Array(1e3).join('123')); | |
| audio.volume = 0.2; | |
| audio.play().catch(e => console.log('Sound playback prevented:', e)); | |
| } | |
| function deleteTask(taskId) { | |
| showConfirm( | |
| 'Delete this task?', | |
| 'This task will be permanently removed from your list.', | |
| () => { | |
| tasks = tasks.filter(task => task.id !== taskId); | |
| saveTasks(); | |
| updateTaskList(); | |
| updateStats(); | |
| } | |
| ); | |
| } | |
| function editTask(taskId, newText) { | |
| const task = tasks.find(task => task.id === taskId); | |
| if (task && newText.trim() !== '') { | |
| task.text = newText.trim(); | |
| saveTasks(); | |
| updateTaskList(); | |
| } | |
| } | |
| function clearCompletedTasks() { | |
| tasks = tasks.filter(task => !task.completed); | |
| saveTasks(); | |
| updateTaskList(); | |
| updateStats(); | |
| } | |
| function setFilter(filter) { | |
| currentFilter = filter; | |
| // Update active filter button | |
| document.querySelectorAll('.filter-btn').forEach(btn => { | |
| btn.classList.remove('active', 'bg-primary-100', 'text-primary-800'); | |
| btn.classList.add('hover:bg-gray-50', 'text-gray-600'); | |
| }); | |
| const activeBtn = document.getElementById(`filter-${filter}`); | |
| activeBtn.classList.add('active', 'bg-primary-100', 'text-primary-800'); | |
| activeBtn.classList.remove('hover:bg-gray-50', 'text-gray-600'); | |
| updateTaskList(); | |
| } | |
| function saveTasks() { | |
| localStorage.setItem('tasks', JSON.stringify(tasks)); | |
| } | |
| function updateStats() { | |
| const total = tasks.length; | |
| const completed = tasks.filter(task => task.completed).length; | |
| const pending = total - completed; | |
| totalTasksEl.textContent = total; | |
| completedTasksEl.textContent = completed; | |
| pendingTasksEl.textContent = pending; | |
| remainingCount.textContent = `${pending} ${pending === 1 ? 'item' : 'items'} left`; | |
| } | |
| function updateTaskList() { | |
| // Filter tasks | |
| let filteredTasks = [...tasks]; | |
| if (currentFilter === 'active') { | |
| filteredTasks = filteredTasks.filter(task => !task.completed); | |
| } else if (currentFilter === 'completed') { | |
| filteredTasks = filteredTasks.filter(task => task.completed); | |
| } | |
| // Sort tasks | |
| filteredTasks.sort((a, b) => { | |
| if (currentSort === 'date') { | |
| return new Date(b.createdAt) - new Date(a.createdAt); | |
| } else if (currentSort === 'priority') { | |
| const priorityOrder = { high: 3, medium: 2, low: 1 }; | |
| return priorityOrder[b.priority] - priorityOrder[a.priority]; | |
| } else if (currentSort === 'name') { | |
| return a.text.localeCompare(b.text); | |
| } | |
| return 0; | |
| }); | |
| // Render tasks | |
| if (filteredTasks.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| taskList.innerHTML = ''; | |
| taskList.appendChild(emptyState); | |
| } else { | |
| emptyState.classList.add('hidden'); | |
| taskList.innerHTML = ''; | |
| filteredTasks.forEach((task, index) => { | |
| const taskElement = createTaskElement(task); | |
| taskList.appendChild(taskElement); | |
| // Add slight delay for staggered animation | |
| setTimeout(() => { | |
| taskElement.classList.add('task-animate'); | |
| }, index * 50); | |
| }); | |
| } | |
| } | |
| function createTaskElement(task) { | |
| const taskElement = document.createElement('div'); | |
| taskElement.className = `task-item border-b border-gray-100 last:border-0 ${task.priority}-priority`; | |
| taskElement.innerHTML = ` | |
| <div class="p-5 flex items-start group"> | |
| <div class="flex items-center h-5 mr-3 mt-0.5"> | |
| <input | |
| type="checkbox" | |
| class="checkbox-custom" | |
| ${task.completed ? 'checked' : ''} | |
| data-id="${task.id}" | |
| > | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div | |
| class="task-text ${task.completed ? 'line-through text-gray-400' : 'text-gray-800'} font-medium" | |
| data-id="${task.id}" | |
| > | |
| ${task.text} | |
| </div> | |
| <div class="flex items-center mt-1.5"> | |
| <div class="text-xs text-gray-500"> | |
| <i class="far fa-clock mr-1"></i>${formatDate(task.createdAt)} | |
| </div> | |
| <span class="ml-2 px-2 py-0.5 rounded-full text-xs font-medium ${getPriorityBadgeClass(task.priority)}"> | |
| ${getPriorityLabel(task.priority)} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="task-actions flex space-x-1 ml-3"> | |
| <button | |
| class="edit-btn p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-primary-600 transition-colors ripple" | |
| data-id="${task.id}" | |
| title="Edit task" | |
| > | |
| <i class="fas fa-pencil-alt text-sm"></i> | |
| </button> | |
| <button | |
| class="priority-btn p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 ${getPriorityBtnClass(task.priority)} transition-colors ripple" | |
| data-id="${task.id}" | |
| title="Change priority" | |
| > | |
| <i class="fas fa-flag text-sm"></i> | |
| </button> | |
| <button | |
| class="delete-btn p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-red-600 transition-colors ripple" | |
| data-id="${task.id}" | |
| title="Delete task" | |
| > | |
| <i class="fas fa-trash-alt text-sm"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| // Add event listeners to the new task | |
| const checkbox = taskElement.querySelector('.checkbox-custom'); | |
| const editBtn = taskElement.querySelector('.edit-btn'); | |
| const deleteBtn = taskElement.querySelector('.delete-btn'); | |
| const priorityBtn = taskElement.querySelector('.priority-btn'); | |
| const taskText = taskElement.querySelector('.task-text'); | |
| checkbox.addEventListener('change', () => toggleTaskComplete(task.id)); | |
| deleteBtn.addEventListener('click', () => deleteTask(task.id)); | |
| priorityBtn.addEventListener('click', () => { | |
| currentTaskIdForPriority = task.id; | |
| showPriorityModal(); | |
| }); | |
| editBtn.addEventListener('click', () => { | |
| const currentText = task.text; | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.value = currentText; | |
| input.className = 'w-full px-2 py-1.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500'; | |
| taskText.innerHTML = ''; | |
| taskText.appendChild(input); | |
| input.focus(); | |
| const handleBlur = () => { | |
| editTask(task.id, input.value); | |
| }; | |
| const handleKeyPress = (e) => { | |
| if (e.key === 'Enter') { | |
| editTask(task.id, input.value); | |
| } | |
| }; | |
| input.addEventListener('blur', handleBlur); | |
| input.addEventListener('keypress', handleKeyPress); | |
| }); | |
| return taskElement; | |
| } | |
| function formatDate(dateString) { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| } | |
| function getPriorityLabel(priority) { | |
| const labels = { | |
| high: 'High', | |
| medium: 'Medium', | |
| low: 'Low' | |
| }; | |
| return labels[priority] || priority; | |
| } | |
| function getPriorityBadgeClass(priority) { | |
| switch (priority) { | |
| case 'high': return 'bg-red-100 text-red-800'; | |
| case 'medium': return 'bg-amber-100 text-amber-800'; | |
| case 'low': return 'bg-green-100 text-green-800'; | |
| default: return 'bg-gray-100 text-gray-800'; | |
| } | |
| } | |
| function getPriorityBtnClass(priority) { | |
| switch (priority) { | |
| case 'high': return 'hover:text-red-600'; | |
| case 'medium': return 'hover:text-amber-600'; | |
| case 'low': return 'hover:text-green-600'; | |
| default: return ''; | |
| } | |
| } | |
| function showConfirm(title, message, callback) { | |
| confirmTitle.textContent = title; | |
| confirmMessage.textContent = message; | |
| confirmCallback = callback; | |
| confirmModal.classList.remove('hidden'); | |
| } | |
| function showPriorityModal() { | |
| priorityModal.classList.remove('hidden'); | |
| // Add animation to modal | |
| const modalContent = document.getElementById('modal-content'); | |
| modalContent.classList.remove('opacity-0', 'scale-95'); | |
| modalContent.classList.add('opacity-100', 'scale-100'); | |
| } | |
| }); | |
| </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=DoraWill/deepsite-todo-app" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |