| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Kanban To-Do Board</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .kanban-column { |
| min-height: 300px; |
| transition: all 0.3s ease; |
| } |
| .task-card { |
| transition: all 0.2s ease; |
| cursor: grab; |
| } |
| .task-card:active { |
| cursor: grabbing; |
| } |
| .task-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
| } |
| .dragging { |
| opacity: 0.5; |
| transform: scale(1.05); |
| } |
| .column-hover { |
| background-color: rgba(226, 232, 240, 0.5); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="flex flex-col md:flex-row justify-between items-center mb-8"> |
| <h1 class="text-3xl font-bold text-indigo-700 mb-4 md:mb-0"> |
| <i class="fas fa-tasks mr-2"></i> Kanban To-Do Board |
| </h1> |
| <div class="flex space-x-2"> |
| <button id="addTaskBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg flex items-center"> |
| <i class="fas fa-plus mr-2"></i> Add Task |
| </button> |
| <button id="clearAllBtn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg flex items-center"> |
| <i class="fas fa-trash-alt mr-2"></i> Clear All |
| </button> |
| </div> |
| </header> |
|
|
| |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> |
| |
| <div class="kanban-column bg-white rounded-lg shadow-md p-4" data-status="backlog"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-lg font-semibold text-gray-700"> |
| <i class="fas fa-inbox mr-2"></i> Backlog |
| </h2> |
| <span id="backlog-count" class="bg-gray-200 text-gray-700 text-xs font-medium px-2.5 py-0.5 rounded-full">0</span> |
| </div> |
| <div class="tasks-container space-y-3 min-h-[200px]" id="backlog-tasks"></div> |
| <button class="add-task-btn mt-3 w-full text-gray-400 hover:text-indigo-600 py-1 rounded" data-status="backlog"> |
| <i class="fas fa-plus mr-1"></i> Add task |
| </button> |
| </div> |
|
|
| |
| <div class="kanban-column bg-white rounded-lg shadow-md p-4" data-status="todo"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-lg font-semibold text-gray-700"> |
| <i class="fas fa-clipboard-list mr-2"></i> To Do |
| </h2> |
| <span id="todo-count" class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">0</span> |
| </div> |
| <div class="tasks-container space-y-3 min-h-[200px]" id="todo-tasks"></div> |
| <button class="add-task-btn mt-3 w-full text-gray-400 hover:text-indigo-600 py-1 rounded" data-status="todo"> |
| <i class="fas fa-plus mr-1"></i> Add task |
| </button> |
| </div> |
|
|
| |
| <div class="kanban-column bg-white rounded-lg shadow-md p-4" data-status="in-progress"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-lg font-semibold text-gray-700"> |
| <i class="fas fa-spinner mr-2"></i> In Progress |
| </h2> |
| <span id="in-progress-count" class="bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded-full">0</span> |
| </div> |
| <div class="tasks-container space-y-3 min-h-[200px]" id="in-progress-tasks"></div> |
| <button class="add-task-btn mt-3 w-full text-gray-400 hover:text-indigo-600 py-1 rounded" data-status="in-progress"> |
| <i class="fas fa-plus mr-1"></i> Add task |
| </button> |
| </div> |
|
|
| |
| <div class="kanban-column bg-white rounded-lg shadow-md p-4" data-status="done"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-lg font-semibold text-gray-700"> |
| <i class="fas fa-check-circle mr-2"></i> Done |
| </h2> |
| <span id="done-count" class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">0</span> |
| </div> |
| <div class="tasks-container space-y-3 min-h-[200px]" id="done-tasks"></div> |
| <button class="add-task-btn mt-3 w-full text-gray-400 hover:text-indigo-600 py-1 rounded" data-status="done"> |
| <i class="fas fa-plus mr-1"></i> Add task |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="taskModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> |
| <div class="flex justify-between items-center border-b px-6 py-4"> |
| <h3 class="text-lg font-semibold text-gray-800" id="modalTitle">Add New Task</h3> |
| <button id="closeModalBtn" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="p-6"> |
| <form id="taskForm"> |
| <input type="hidden" id="taskId"> |
| <div class="mb-4"> |
| <label for="taskTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label> |
| <input type="text" id="taskTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" required> |
| </div> |
| <div class="mb-4"> |
| <label for="taskDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label> |
| <textarea id="taskDescription" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea> |
| </div> |
| <div class="mb-4"> |
| <label for="taskStatus" class="block text-sm font-medium text-gray-700 mb-1">Status</label> |
| <select id="taskStatus" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| <option value="backlog">Backlog</option> |
| <option value="todo">To Do</option> |
| <option value="in-progress">In Progress</option> |
| <option value="done">Done</option> |
| </select> |
| </div> |
| <div class="mb-4"> |
| <label for="taskPriority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label> |
| <select id="taskPriority" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| <option value="low">Low</option> |
| <option value="medium" selected>Medium</option> |
| <option value="high">High</option> |
| </select> |
| </div> |
| <div class="mb-4"> |
| <label for="taskDueDate" class="block text-sm font-medium text-gray-700 mb-1">Due Date</label> |
| <input type="date" id="taskDueDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| </div> |
| </form> |
| </div> |
| <div class="flex justify-end space-x-3 border-t px-6 py-4"> |
| <button id="cancelTaskBtn" class="px-4 py-2 text-gray-600 hover:text-gray-800">Cancel</button> |
| <button id="saveTaskBtn" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Save Task</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="confirmModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> |
| <div class="flex justify-between items-center border-b px-6 py-4"> |
| <h3 class="text-lg font-semibold text-gray-800">Confirm Action</h3> |
| <button id="closeConfirmModalBtn" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="p-6"> |
| <p id="confirmMessage" class="text-gray-700">Are you sure you want to perform this action?</p> |
| </div> |
| <div class="flex justify-end space-x-3 border-t px-6 py-4"> |
| <button id="cancelConfirmBtn" class="px-4 py-2 text-gray-600 hover:text-gray-800">Cancel</button> |
| <button id="confirmActionBtn" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">Confirm</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const kanbanBoard = document.querySelector('.grid'); |
| const columns = document.querySelectorAll('.kanban-column'); |
| const taskContainers = { |
| 'backlog': document.getElementById('backlog-tasks'), |
| 'todo': document.getElementById('todo-tasks'), |
| 'in-progress': document.getElementById('in-progress-tasks'), |
| 'done': document.getElementById('done-tasks') |
| }; |
| const countElements = { |
| 'backlog': document.getElementById('backlog-count'), |
| 'todo': document.getElementById('todo-count'), |
| 'in-progress': document.getElementById('in-progress-count'), |
| 'done': document.getElementById('done-count') |
| }; |
| |
| |
| const taskModal = document.getElementById('taskModal'); |
| const confirmModal = document.getElementById('confirmModal'); |
| const taskForm = document.getElementById('taskForm'); |
| const modalTitle = document.getElementById('modalTitle'); |
| const taskIdInput = document.getElementById('taskId'); |
| const taskTitleInput = document.getElementById('taskTitle'); |
| const taskDescriptionInput = document.getElementById('taskDescription'); |
| const taskStatusInput = document.getElementById('taskStatus'); |
| const taskPriorityInput = document.getElementById('taskPriority'); |
| const taskDueDateInput = document.getElementById('taskDueDate'); |
| const confirmMessage = document.getElementById('confirmMessage'); |
| |
| |
| let tasks = JSON.parse(localStorage.getItem('kanban-tasks')) || []; |
| let currentTaskId = null; |
| let confirmAction = null; |
| |
| |
| function init() { |
| renderTasks(); |
| setupEventListeners(); |
| setupDragAndDrop(); |
| } |
| |
| |
| function renderTasks() { |
| |
| Object.values(taskContainers).forEach(container => { |
| container.innerHTML = ''; |
| }); |
| |
| |
| Object.values(countElements).forEach(element => { |
| element.textContent = '0'; |
| }); |
| |
| |
| tasks.forEach(task => { |
| addTaskToDOM(task); |
| updateCounter(task.status); |
| }); |
| } |
| |
| |
| function addTaskToDOM(task) { |
| const taskElement = createTaskElement(task); |
| taskContainers[task.status].appendChild(taskElement); |
| } |
| |
| |
| function createTaskElement(task) { |
| const priorityColors = { |
| 'low': 'bg-blue-100 text-blue-800', |
| 'medium': 'bg-yellow-100 text-yellow-800', |
| 'high': 'bg-red-100 text-red-800' |
| }; |
| |
| const priorityText = { |
| 'low': 'Low', |
| 'medium': 'Medium', |
| 'high': 'High' |
| }; |
| |
| const taskElement = document.createElement('div'); |
| taskElement.className = 'task-card bg-white p-3 rounded-lg shadow-sm border border-gray-200'; |
| taskElement.dataset.taskId = task.id; |
| taskElement.draggable = true; |
| |
| const dueDate = task.dueDate ? new Date(task.dueDate) : null; |
| const today = new Date(); |
| today.setHours(0, 0, 0, 0); |
| |
| let dueDateBadge = ''; |
| if (dueDate) { |
| const dueDateStr = dueDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); |
| const isOverdue = dueDate < today && task.status !== 'done'; |
| |
| if (isOverdue) { |
| dueDateBadge = `<span class="text-xs font-medium px-2 py-0.5 rounded-full bg-red-100 text-red-800"> |
| <i class="fas fa-exclamation-circle mr-1"></i>Overdue: ${dueDateStr} |
| </span>`; |
| } else { |
| dueDateBadge = `<span class="text-xs font-medium px-2 py-0.5 rounded-full bg-gray-100 text-gray-800"> |
| <i class="far fa-calendar-alt mr-1"></i>${dueDateStr} |
| </span>`; |
| } |
| } |
| |
| taskElement.innerHTML = ` |
| <div class="flex justify-between items-start mb-2"> |
| <h3 class="font-medium text-gray-800">${task.title}</h3> |
| <div class="flex space-x-1"> |
| <button class="edit-task-btn text-gray-400 hover:text-indigo-600" data-task-id="${task.id}"> |
| <i class="fas fa-edit text-sm"></i> |
| </button> |
| <button class="delete-task-btn text-gray-400 hover:text-red-600" data-task-id="${task.id}"> |
| <i class="fas fa-trash-alt text-sm"></i> |
| </button> |
| </div> |
| </div> |
| ${task.description ? `<p class="text-sm text-gray-600 mb-2">${task.description}</p>` : ''} |
| <div class="flex justify-between items-center"> |
| <span class="text-xs font-medium px-2 py-0.5 rounded-full ${priorityColors[task.priority]}"> |
| ${priorityText[task.priority]} |
| </span> |
| ${dueDateBadge} |
| </div> |
| `; |
| |
| return taskElement; |
| } |
| |
| |
| function updateCounter(status) { |
| const count = tasks.filter(task => task.status === status).length; |
| countElements[status].textContent = count; |
| } |
| |
| |
| function saveTasks() { |
| localStorage.setItem('kanban-tasks', JSON.stringify(tasks)); |
| } |
| |
| |
| function generateId() { |
| return Date.now().toString(36) + Math.random().toString(36).substr(2); |
| } |
| |
| |
| function openTaskModal(task = null) { |
| if (task) { |
| |
| modalTitle.textContent = 'Edit Task'; |
| taskIdInput.value = task.id; |
| taskTitleInput.value = task.title; |
| taskDescriptionInput.value = task.description || ''; |
| taskStatusInput.value = task.status; |
| taskPriorityInput.value = task.priority || 'medium'; |
| taskDueDateInput.value = task.dueDate || ''; |
| currentTaskId = task.id; |
| } else { |
| |
| modalTitle.textContent = 'Add New Task'; |
| taskForm.reset(); |
| taskIdInput.value = ''; |
| currentTaskId = null; |
| } |
| taskModal.classList.remove('hidden'); |
| } |
| |
| |
| function closeTaskModal() { |
| taskModal.classList.add('hidden'); |
| } |
| |
| |
| function openConfirmModal(message, action) { |
| confirmMessage.textContent = message; |
| confirmAction = action; |
| confirmModal.classList.remove('hidden'); |
| } |
| |
| |
| function closeConfirmModal() { |
| confirmModal.classList.add('hidden'); |
| confirmAction = null; |
| } |
| |
| |
| function addTask(taskData) { |
| const newTask = { |
| id: generateId(), |
| title: taskData.title, |
| description: taskData.description, |
| status: taskData.status, |
| priority: taskData.priority, |
| dueDate: taskData.dueDate, |
| createdAt: new Date().toISOString() |
| }; |
| |
| tasks.push(newTask); |
| saveTasks(); |
| addTaskToDOM(newTask); |
| updateCounter(newTask.status); |
| } |
| |
| |
| function updateTask(taskId, updatedData) { |
| const taskIndex = tasks.findIndex(task => task.id === taskId); |
| if (taskIndex !== -1) { |
| const originalStatus = tasks[taskIndex].status; |
| |
| |
| tasks[taskIndex] = { |
| ...tasks[taskIndex], |
| ...updatedData |
| }; |
| |
| saveTasks(); |
| |
| |
| if (originalStatus !== updatedData.status) { |
| renderTasks(); |
| } else { |
| |
| const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); |
| if (taskElement) { |
| const newTaskElement = createTaskElement(tasks[taskIndex]); |
| taskElement.replaceWith(newTaskElement); |
| } |
| } |
| } |
| } |
| |
| |
| function deleteTask(taskId) { |
| const taskIndex = tasks.findIndex(task => task.id === taskId); |
| if (taskIndex !== -1) { |
| const status = tasks[taskIndex].status; |
| tasks.splice(taskIndex, 1); |
| saveTasks(); |
| |
| |
| const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); |
| if (taskElement) { |
| taskElement.remove(); |
| } |
| |
| updateCounter(status); |
| } |
| } |
| |
| |
| function clearAllTasks() { |
| tasks = []; |
| saveTasks(); |
| renderTasks(); |
| } |
| |
| |
| function setupDragAndDrop() { |
| let draggedTask = null; |
| |
| |
| document.addEventListener('dragstart', function(e) { |
| if (e.target.classList.contains('task-card')) { |
| draggedTask = e.target; |
| e.target.classList.add('dragging'); |
| |
| |
| setTimeout(() => { |
| e.target.classList.add('hidden'); |
| }, 0); |
| } |
| }); |
| |
| |
| document.addEventListener('dragend', function(e) { |
| if (e.target.classList.contains('task-card')) { |
| e.target.classList.remove('dragging', 'hidden'); |
| draggedTask = null; |
| |
| |
| columns.forEach(column => { |
| column.classList.remove('column-hover'); |
| }); |
| } |
| }); |
| |
| |
| columns.forEach(column => { |
| column.addEventListener('dragover', function(e) { |
| e.preventDefault(); |
| column.classList.add('column-hover'); |
| }); |
| |
| column.addEventListener('dragleave', function() { |
| column.classList.remove('column-hover'); |
| }); |
| |
| column.addEventListener('drop', function(e) { |
| e.preventDefault(); |
| column.classList.remove('column-hover'); |
| |
| if (draggedTask) { |
| const taskId = draggedTask.dataset.taskId; |
| const newStatus = column.dataset.status; |
| |
| |
| const taskIndex = tasks.findIndex(task => task.id === taskId); |
| if (taskIndex !== -1 && tasks[taskIndex].status !== newStatus) { |
| tasks[taskIndex].status = newStatus; |
| saveTasks(); |
| |
| |
| const tasksContainer = column.querySelector('.tasks-container'); |
| tasksContainer.appendChild(draggedTask); |
| |
| |
| updateCounter(newStatus); |
| updateCounter(tasks[taskIndex].status); |
| } |
| } |
| }); |
| }); |
| } |
| |
| |
| function setupEventListeners() { |
| |
| document.getElementById('addTaskBtn').addEventListener('click', () => openTaskModal()); |
| |
| |
| document.querySelectorAll('.add-task-btn').forEach(button => { |
| button.addEventListener('click', function() { |
| openTaskModal({ |
| status: this.dataset.status |
| }); |
| }); |
| }); |
| |
| |
| document.getElementById('closeModalBtn').addEventListener('click', closeTaskModal); |
| document.getElementById('cancelTaskBtn').addEventListener('click', closeTaskModal); |
| document.getElementById('saveTaskBtn').addEventListener('click', function() { |
| if (taskTitleInput.value.trim() === '') { |
| alert('Task title is required'); |
| return; |
| } |
| |
| const taskData = { |
| title: taskTitleInput.value.trim(), |
| description: taskDescriptionInput.value.trim(), |
| status: taskStatusInput.value, |
| priority: taskPriorityInput.value, |
| dueDate: taskDueDateInput.value |
| }; |
| |
| if (currentTaskId) { |
| updateTask(currentTaskId, taskData); |
| } else { |
| addTask(taskData); |
| } |
| |
| closeTaskModal(); |
| }); |
| |
| |
| document.getElementById('closeConfirmModalBtn').addEventListener('click', closeConfirmModal); |
| document.getElementById('cancelConfirmBtn').addEventListener('click', closeConfirmModal); |
| document.getElementById('confirmActionBtn').addEventListener('click', function() { |
| if (confirmAction) { |
| confirmAction(); |
| } |
| closeConfirmModal(); |
| }); |
| |
| |
| document.getElementById('clearAllBtn').addEventListener('click', function() { |
| if (tasks.length > 0) { |
| openConfirmModal('Are you sure you want to delete all tasks? This action cannot be undone.', clearAllTasks); |
| } else { |
| alert('There are no tasks to clear.'); |
| } |
| }); |
| |
| |
| kanbanBoard.addEventListener('click', function(e) { |
| |
| if (e.target.closest('.edit-task-btn')) { |
| const taskId = e.target.closest('.edit-task-btn').dataset.taskId; |
| const task = tasks.find(task => task.id === taskId); |
| if (task) { |
| openTaskModal(task); |
| } |
| } |
| |
| |
| if (e.target.closest('.delete-task-btn')) { |
| const taskId = e.target.closest('.delete-task-btn').dataset.taskId; |
| const task = tasks.find(task => task.id === taskId); |
| |
| if (task) { |
| openConfirmModal('Are you sure you want to delete this task?', () => { |
| deleteTask(taskId); |
| }); |
| } |
| } |
| }); |
| } |
| |
| |
| init(); |
| }); |
| </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=XOHDY/kanban" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |