kanban / index.html
XOHDY's picture
need you to create new app to do list as Kanban board, save all things localy so the user not lost the data - Initial Deployment
21f2bde verified
<!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 -->
<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>
<!-- Kanban Board -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Backlog Column -->
<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>
<!-- To Do Column -->
<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>
<!-- In Progress Column -->
<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>
<!-- Done Column -->
<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>
<!-- Add/Edit Task Modal -->
<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>
<!-- Confirmation Modal -->
<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() {
// DOM Elements
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')
};
// Modal Elements
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');
// State
let tasks = JSON.parse(localStorage.getItem('kanban-tasks')) || [];
let currentTaskId = null;
let confirmAction = null;
// Initialize the app
function init() {
renderTasks();
setupEventListeners();
setupDragAndDrop();
}
// Render all tasks to the board
function renderTasks() {
// Clear all task containers first
Object.values(taskContainers).forEach(container => {
container.innerHTML = '';
});
// Reset all counters
Object.values(countElements).forEach(element => {
element.textContent = '0';
});
// Render each task to its appropriate column
tasks.forEach(task => {
addTaskToDOM(task);
updateCounter(task.status);
});
}
// Add a single task to the DOM
function addTaskToDOM(task) {
const taskElement = createTaskElement(task);
taskContainers[task.status].appendChild(taskElement);
}
// Create a task element
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;
}
// Update counter for a specific status
function updateCounter(status) {
const count = tasks.filter(task => task.status === status).length;
countElements[status].textContent = count;
}
// Save tasks to localStorage
function saveTasks() {
localStorage.setItem('kanban-tasks', JSON.stringify(tasks));
}
// Generate a unique ID for new tasks
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Open the task modal for adding/editing
function openTaskModal(task = null) {
if (task) {
// Edit mode
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 {
// Add mode
modalTitle.textContent = 'Add New Task';
taskForm.reset();
taskIdInput.value = '';
currentTaskId = null;
}
taskModal.classList.remove('hidden');
}
// Close the task modal
function closeTaskModal() {
taskModal.classList.add('hidden');
}
// Open confirmation modal
function openConfirmModal(message, action) {
confirmMessage.textContent = message;
confirmAction = action;
confirmModal.classList.remove('hidden');
}
// Close confirmation modal
function closeConfirmModal() {
confirmModal.classList.add('hidden');
confirmAction = null;
}
// Add a new task
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);
}
// Update an existing task
function updateTask(taskId, updatedData) {
const taskIndex = tasks.findIndex(task => task.id === taskId);
if (taskIndex !== -1) {
const originalStatus = tasks[taskIndex].status;
// Update task data
tasks[taskIndex] = {
...tasks[taskIndex],
...updatedData
};
saveTasks();
// If status changed, we need to move the task
if (originalStatus !== updatedData.status) {
renderTasks(); // Full re-render is simpler for status change
} else {
// Just update the existing task element
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskElement) {
const newTaskElement = createTaskElement(tasks[taskIndex]);
taskElement.replaceWith(newTaskElement);
}
}
}
}
// Delete a task
function deleteTask(taskId) {
const taskIndex = tasks.findIndex(task => task.id === taskId);
if (taskIndex !== -1) {
const status = tasks[taskIndex].status;
tasks.splice(taskIndex, 1);
saveTasks();
// Remove from DOM
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskElement) {
taskElement.remove();
}
updateCounter(status);
}
}
// Clear all tasks
function clearAllTasks() {
tasks = [];
saveTasks();
renderTasks();
}
// Setup drag and drop functionality
function setupDragAndDrop() {
let draggedTask = null;
// Drag start event
document.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('task-card')) {
draggedTask = e.target;
e.target.classList.add('dragging');
// Set the drag image to be the task card itself
setTimeout(() => {
e.target.classList.add('hidden');
}, 0);
}
});
// Drag end event
document.addEventListener('dragend', function(e) {
if (e.target.classList.contains('task-card')) {
e.target.classList.remove('dragging', 'hidden');
draggedTask = null;
// Remove hover styles from all columns
columns.forEach(column => {
column.classList.remove('column-hover');
});
}
});
// Drag over event for columns
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;
// Update task status
const taskIndex = tasks.findIndex(task => task.id === taskId);
if (taskIndex !== -1 && tasks[taskIndex].status !== newStatus) {
tasks[taskIndex].status = newStatus;
saveTasks();
// Move task to new column
const tasksContainer = column.querySelector('.tasks-container');
tasksContainer.appendChild(draggedTask);
// Update counters
updateCounter(newStatus);
updateCounter(tasks[taskIndex].status); // Original status
}
}
});
});
}
// Setup event listeners
function setupEventListeners() {
// Add task button
document.getElementById('addTaskBtn').addEventListener('click', () => openTaskModal());
// Column add task buttons
document.querySelectorAll('.add-task-btn').forEach(button => {
button.addEventListener('click', function() {
openTaskModal({
status: this.dataset.status
});
});
});
// Modal buttons
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();
});
// Confirmation modal buttons
document.getElementById('closeConfirmModalBtn').addEventListener('click', closeConfirmModal);
document.getElementById('cancelConfirmBtn').addEventListener('click', closeConfirmModal);
document.getElementById('confirmActionBtn').addEventListener('click', function() {
if (confirmAction) {
confirmAction();
}
closeConfirmModal();
});
// Clear all button
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.');
}
});
// Edit and delete buttons (delegated events since tasks are dynamic)
kanbanBoard.addEventListener('click', function(e) {
// Edit task
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);
}
}
// Delete 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);
});
}
}
});
}
// Initialize the app
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>