Added some WIP assets for the Local/World/UI views and the cover art.
This commit is contained in:
217
tools/kanban/kanban.html
Normal file
217
tools/kanban/kanban.html
Normal file
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simple Kanban Board</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="src/styles.css">
|
||||
<link rel="icon" type="image/x-icon" href="src/kanban.ico">
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800" id="main-body">
|
||||
|
||||
<!-- Dark mode toggle in upper right -->
|
||||
<div class="fixed top-4 right-4 z-50">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" id="dark-mode-toggle" class="sr-only">
|
||||
<span class="w-10 h-6 flex items-center bg-gray-300 rounded-full p-1 transition-colors duration-200">
|
||||
<span class="dot w-4 h-4 bg-white rounded-full shadow-md transform transition-transform duration-200"></span>
|
||||
</span>
|
||||
<span class="ml-2">
|
||||
<svg id="darkmode-lightbulb" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 2a7 7 0 017 7c0 3.5-2.5 5.5-3.5 6.5-.5.5-.5 1.5-.5 2.5v1a1 1 0 01-1 1h-2a1 1 0 01-1-1v-1c0-1-.1-2 .5-2.5C7.5 14.5 5 12.5 5 9a7 7 0 017-7zm0 20a2 2 0 002-2H10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900">Kanban Board</h1>
|
||||
<p class="text-gray-600 mt-2">Drag and drop tasks to organize your workflow.</p>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 mt-4 mb-6 justify-between">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto justify-start">
|
||||
<button class="add-task-btn flex items-center justify-center w-10 h-10 bg-red-500 rounded-full shadow hover:bg-red-600 transition duration-150" data-column="todo-column" style="border: none; padding: 0;">
|
||||
<span class="sr-only">Add Task</span>
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||
<line x1="12" y1="6" x2="12" y2="18" />
|
||||
<line x1="6" y1="12" x2="18" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="ml-2 text-gray-700 font-medium text-base hidden sm:inline">Add Task</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
<label for="tag-filter" class="text-gray-700 font-medium text-base mr-2">Filter by Tag:</label>
|
||||
<select id="tag-filter" class="border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-red-400 shadow-sm w-full sm:w-48">
|
||||
<option value="all">All</option>
|
||||
<option value="unassigned">Unassigned</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kanban Board Columns -->
|
||||
<div id="kanban-board" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<!-- To Do Column -->
|
||||
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="todo-column">
|
||||
<div class="flex justify-between items-center pb-2 border-b-2 border-red-400 mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">To Do</h2>
|
||||
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="todo-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||
</div>
|
||||
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="todo">
|
||||
<!-- Tasks Go Here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In Progress Column -->
|
||||
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="inprogress-column">
|
||||
<div class="flex justify-between items-center pb-2 border-b-2 border-yellow-400 mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">In Progress</h2>
|
||||
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="inprogress-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||
</div>
|
||||
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="inprogress">
|
||||
<!-- Tasks Go Here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Column -->
|
||||
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="completed-column">
|
||||
<div class="flex justify-between items-center pb-2 border-b-2 border-green-400 mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">Completed</h2>
|
||||
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="completed-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||
</div>
|
||||
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="completed">
|
||||
<!-- Tasks Go Here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Task Modal -->
|
||||
<div id="task-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-md">
|
||||
<h3 class="text-lg font-bold mb-4" id="modal-title">Add New Task</h3>
|
||||
<textarea id="task-input" class="w-full border border-gray-300 rounded-md p-2" placeholder="Enter task description..."></textarea>
|
||||
<div class="mt-4">
|
||||
<label for="task-due-date" class="block text-sm font-medium text-gray-700">Due Date</label>
|
||||
<input type="date" id="task-due-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="stakeholders-input" class="block text-sm font-medium text-gray-700">Key Stakeholders (optional)</label>
|
||||
<input type="text" id="stakeholders-input" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Enter key stakeholders...">
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="notes-input" class="block text-sm font-medium text-gray-700">Notes (optional)</label>
|
||||
<textarea id="notes-input" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Enter notes..."></textarea>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<button id="cancel-task" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancel</button>
|
||||
<button id="save-task" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tag Management Modal -->
|
||||
<div id="tag-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-md">
|
||||
<h3 class="text-lg font-bold mb-4">Manage Tag</h3>
|
||||
<div id="existing-tags-container" class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Select Existing Tag</label>
|
||||
<div id="tags-list" class="flex flex-wrap gap-2">
|
||||
<!-- Existing tags will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div>
|
||||
<label for="new-tag-name" class="block text-sm font-medium text-gray-700">Or Create New Tag</label>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input type="text" id="new-tag-name" class="flex-grow border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Tag name...">
|
||||
<input type="color" id="new-tag-color" class="w-10 h-10 p-1 border border-gray-300 rounded-md cursor-pointer" value="#3b82f6">
|
||||
<button id="save-tag" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="remove-tag-name" class="block text-sm font-medium text-gray-700">Or Remove Tag</label>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<button id="remove-tag" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600">Remove Tag</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-2">
|
||||
<button id="cancel-tag" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||
<h3 class="text-lg font-bold mb-4">Confirm Deletion</h3>
|
||||
<p class="text-gray-600">Are you sure you want to delete this task?</p>
|
||||
<div class="mt-6 flex justify-center space-x-4">
|
||||
<button id="cancel-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||
<button id="confirm-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Details Modal -->
|
||||
<div id="task-details-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold">Task Details</h3>
|
||||
<button id="close-task-details" class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<!-- Task Description -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-sm font-medium text-gray-700">Task Description</label>
|
||||
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="description">Edit</button>
|
||||
</div>
|
||||
<div id="display-description" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||
<textarea id="edit-description" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="3"></textarea>
|
||||
</div>
|
||||
<!-- Due Date -->
|
||||
<div>
|
||||
<label for="edit-due-date" class="block text-sm font-medium text-gray-700">Due Date</label>
|
||||
<input type="date" id="edit-due-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<!-- Date Started -->
|
||||
<div>
|
||||
<label for="edit-started-date" class="block text-sm font-medium text-gray-700">Date Started</label>
|
||||
<input type="date" id="edit-started-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<!-- Stakeholders -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-sm font-medium text-gray-700">Key Stakeholders</label>
|
||||
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="stakeholders">Edit</button>
|
||||
</div>
|
||||
<div id="display-stakeholders" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||
<textarea id="edit-stakeholders" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="2"></textarea>
|
||||
</div>
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-sm font-medium text-gray-700">Notes</label>
|
||||
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="notes">Edit</button>
|
||||
</div>
|
||||
<div id="display-notes" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||
<textarea id="edit-notes" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-2">
|
||||
<button id="save-task-changes" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="src/SimpleKanban.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
916
tools/kanban/src/SimpleKanban.js
Normal file
916
tools/kanban/src/SimpleKanban.js
Normal file
@@ -0,0 +1,916 @@
|
||||
// Tag delete confirmation modal logic
|
||||
let tagToDelete = null;
|
||||
let tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||
let tagDeleteNameSpan = null;
|
||||
let tagDeleteConfirmBtn = null;
|
||||
let tagDeleteCancelBtn = null;
|
||||
function ensureTagDeleteModal() {
|
||||
tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||
if (!tagDeleteModal) {
|
||||
tagDeleteModal = document.createElement('div');
|
||||
tagDeleteModal.id = 'tag-delete-modal';
|
||||
tagDeleteModal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden';
|
||||
tagDeleteModal.innerHTML = `
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||
<h3 class="text-lg font-bold mb-4">Delete Tag</h3>
|
||||
<p class="text-gray-600 mb-2">Are you sure you want to delete the tag <span id="tag-delete-name" class="font-bold"></span>? This will remove the tag from all tasks.</p>
|
||||
<div class="mt-6 flex justify-center space-x-4">
|
||||
<button id="cancel-tag-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||
<button id="confirm-tag-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(tagDeleteModal);
|
||||
}
|
||||
tagDeleteNameSpan = tagDeleteModal.querySelector('#tag-delete-name');
|
||||
tagDeleteConfirmBtn = tagDeleteModal.querySelector('#confirm-tag-delete');
|
||||
tagDeleteCancelBtn = tagDeleteModal.querySelector('#cancel-tag-delete');
|
||||
}
|
||||
ensureTagDeleteModal();
|
||||
function showTagDeleteModal(tagName) {
|
||||
tagToDelete = tagName;
|
||||
ensureTagDeleteModal();
|
||||
tagDeleteNameSpan.textContent = tagName;
|
||||
tagDeleteModal.classList.remove('hidden');
|
||||
tagDeleteCancelBtn.onclick = hideTagDeleteModal;
|
||||
tagDeleteConfirmBtn.onclick = function() {
|
||||
if (tagToDelete) {
|
||||
// Remove tag from global tags
|
||||
delete tags[tagToDelete];
|
||||
// Remove tag from all tasks
|
||||
document.querySelectorAll('.task').forEach(task => {
|
||||
if (task.dataset.tagName === tagToDelete) {
|
||||
delete task.dataset.tagName;
|
||||
delete task.dataset.tagColor;
|
||||
const tagCircle = task.querySelector('.tag-circle');
|
||||
if (tagCircle) {
|
||||
tagCircle.style.backgroundColor = 'transparent';
|
||||
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||
}
|
||||
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||
if (tooltipOverlay) {
|
||||
tooltipOverlay.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
openTagModal();
|
||||
hideTagDeleteModal();
|
||||
}
|
||||
};
|
||||
}
|
||||
function hideTagDeleteModal() {
|
||||
tagDeleteModal.classList.add('hidden');
|
||||
tagToDelete = null;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let draggedTask = null;
|
||||
let targetColumn = null;
|
||||
let taskToDelete = null;
|
||||
let currentTaskForTagging = null;
|
||||
|
||||
// Global tags object
|
||||
let tags = {
|
||||
// Example: "UI/UX": "#3b82f6",
|
||||
};
|
||||
|
||||
// Tag delete confirmation modal logic (move inside DOMContentLoaded for correct tags reference)
|
||||
let tagToDelete = null;
|
||||
let tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||
let tagDeleteNameSpan = null;
|
||||
let tagDeleteConfirmBtn = null;
|
||||
let tagDeleteCancelBtn = null;
|
||||
function ensureTagDeleteModal() {
|
||||
tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||
if (!tagDeleteModal) {
|
||||
tagDeleteModal = document.createElement('div');
|
||||
tagDeleteModal.id = 'tag-delete-modal';
|
||||
tagDeleteModal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden';
|
||||
tagDeleteModal.innerHTML = `
|
||||
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||
<h3 class="text-lg font-bold mb-4">Delete Tag</h3>
|
||||
<p class="text-gray-600 mb-2">Are you sure you want to delete the tag <span id="tag-delete-name" class="font-bold"></span>? This will remove the tag from all tasks.</p>
|
||||
<div class="mt-6 flex justify-center space-x-4">
|
||||
<button id="cancel-tag-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||
<button id="confirm-tag-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(tagDeleteModal);
|
||||
}
|
||||
tagDeleteNameSpan = tagDeleteModal.querySelector('#tag-delete-name');
|
||||
tagDeleteConfirmBtn = tagDeleteModal.querySelector('#confirm-tag-delete');
|
||||
tagDeleteCancelBtn = tagDeleteModal.querySelector('#cancel-tag-delete');
|
||||
}
|
||||
ensureTagDeleteModal();
|
||||
function showTagDeleteModal(tagName) {
|
||||
tagToDelete = tagName;
|
||||
ensureTagDeleteModal();
|
||||
tagDeleteNameSpan.textContent = tagName;
|
||||
tagDeleteModal.classList.remove('hidden');
|
||||
tagDeleteCancelBtn.onclick = hideTagDeleteModal;
|
||||
tagDeleteConfirmBtn.onclick = function() {
|
||||
if (tagToDelete) {
|
||||
// Remove tag from global tags
|
||||
delete tags[tagToDelete];
|
||||
// Remove tag from all tasks
|
||||
document.querySelectorAll('.task').forEach(task => {
|
||||
if (task.dataset.tagName === tagToDelete) {
|
||||
delete task.dataset.tagName;
|
||||
delete task.dataset.tagColor;
|
||||
const tagCircle = task.querySelector('.tag-circle');
|
||||
if (tagCircle) {
|
||||
tagCircle.style.backgroundColor = 'transparent';
|
||||
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||
}
|
||||
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||
if (tooltipOverlay) {
|
||||
tooltipOverlay.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
openTagModal();
|
||||
hideTagDeleteModal();
|
||||
}
|
||||
};
|
||||
}
|
||||
function hideTagDeleteModal() {
|
||||
tagDeleteModal.classList.add('hidden');
|
||||
tagToDelete = null;
|
||||
}
|
||||
|
||||
const tagModal = document.getElementById('tag-modal');
|
||||
const cancelTagBtn = document.getElementById('cancel-tag');
|
||||
const saveTagBtn = document.getElementById('save-tag');
|
||||
const removeTagBtn = document.getElementById('remove-tag');
|
||||
const newTagNameInput = document.getElementById('new-tag-name');
|
||||
const newTagColorInput = document.getElementById('new-tag-color');
|
||||
const tagsListContainer = document.getElementById('tags-list');
|
||||
|
||||
// Function to initialize event listeners for a task
|
||||
function initializeTaskEvents(task) {
|
||||
task.addEventListener('dragstart', () => {
|
||||
draggedTask = task;
|
||||
setTimeout(() => {
|
||||
task.classList.add('dragging');
|
||||
}, 0);
|
||||
});
|
||||
|
||||
task.addEventListener('dragend', () => {
|
||||
task.classList.remove('dragging');
|
||||
draggedTask = null;
|
||||
});
|
||||
|
||||
// Double-click to open details modal
|
||||
task.addEventListener('dblclick', (e) => {
|
||||
e.stopPropagation();
|
||||
openTaskDetailsModal(task);
|
||||
});
|
||||
|
||||
const deleteBtn = task.querySelector('.delete-task-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
taskToDelete = task;
|
||||
deleteModal.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
const tagCircle = task.querySelector('.tag-circle');
|
||||
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||
|
||||
if (tagCircle && tooltipOverlay) {
|
||||
tagCircle.addEventListener('mouseenter', () => {
|
||||
if (task.dataset.tagName) {
|
||||
tooltipOverlay.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
tagCircle.addEventListener('mouseleave', () => {
|
||||
tooltipOverlay.classList.add('hidden');
|
||||
});
|
||||
|
||||
tagCircle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
currentTaskForTagging = task;
|
||||
openTagModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to open and populate the tag modal
|
||||
function openTagModal() {
|
||||
tagsListContainer.innerHTML = '';
|
||||
Object.entries(tags).forEach(([name, color]) => {
|
||||
const tagChip = document.createElement('div');
|
||||
tagChip.className = 'px-3 py-1 rounded-full text-sm font-medium cursor-pointer flex items-center gap-2';
|
||||
tagChip.style.backgroundColor = color;
|
||||
tagChip.style.color = getContrastYIQ(color);
|
||||
tagChip.textContent = name;
|
||||
tagChip.dataset.tagName = name;
|
||||
tagChip.dataset.tagColor = color;
|
||||
tagChip.addEventListener('click', () => {
|
||||
applyTagToTask(name, color);
|
||||
closeTagModal();
|
||||
});
|
||||
// Add delete button for tag
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'ml-2 text-xs text-white bg-red-600 rounded-full w-5 h-5 flex items-center justify-center hover:bg-red-800';
|
||||
deleteBtn.innerHTML = '<span>×</span>';
|
||||
deleteBtn.title = 'Delete Tag';
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
showTagDeleteModal(name);
|
||||
});
|
||||
tagChip.appendChild(deleteBtn);
|
||||
tagsListContainer.appendChild(tagChip);
|
||||
});
|
||||
newTagNameInput.value = '';
|
||||
tagModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTagModal() {
|
||||
tagModal.classList.add('hidden');
|
||||
currentTaskForTagging = null;
|
||||
}
|
||||
|
||||
function applyTagToTask(name, color) {
|
||||
if (!currentTaskForTagging) return;
|
||||
const tagCircle = currentTaskForTagging.querySelector('.tag-circle');
|
||||
currentTaskForTagging.dataset.tagName = name;
|
||||
currentTaskForTagging.dataset.tagColor = color;
|
||||
tagCircle.style.backgroundColor = color;
|
||||
tagCircle.classList.remove('border-2', 'border-gray-400');
|
||||
// Add or update tooltip
|
||||
const tooltipOverlay = currentTaskForTagging.querySelector('.task-tooltip-overlay');
|
||||
if (tooltipOverlay) {
|
||||
tooltipOverlay.textContent = name;
|
||||
}
|
||||
}
|
||||
|
||||
function removeTagFromTask() {
|
||||
if (!currentTaskForTagging) return;
|
||||
const tagCircle = currentTaskForTagging.querySelector('.tag-circle');
|
||||
delete currentTaskForTagging.dataset.tagName;
|
||||
delete currentTaskForTagging.dataset.tagColor;
|
||||
tagCircle.style.backgroundColor = 'transparent';
|
||||
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||
const tooltipOverlay = currentTaskForTagging.querySelector('.task-tooltip-overlay');
|
||||
if (tooltipOverlay) {
|
||||
tooltipOverlay.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
saveTagBtn.addEventListener('click', () => {
|
||||
const newName = newTagNameInput.value.trim();
|
||||
const newColor = newTagColorInput.value;
|
||||
if (!newName) {
|
||||
newTagNameInput.focus();
|
||||
return; // Do not close modal if name is empty
|
||||
}
|
||||
// Add to global tags if it's new
|
||||
if (!tags[newName]) {
|
||||
tags[newName] = newColor;
|
||||
}
|
||||
applyTagToTask(newName, newColor);
|
||||
closeTagModal();
|
||||
});
|
||||
|
||||
removeTagBtn.addEventListener('click', () => {
|
||||
removeTagFromTask();
|
||||
closeTagModal();
|
||||
});
|
||||
|
||||
cancelTagBtn.addEventListener('click', closeTagModal);
|
||||
|
||||
// Linkify function for emails and URLs
|
||||
function linkifyText(text) {
|
||||
if (!text) return '';
|
||||
// Linkify emails
|
||||
text = text.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||
// Linkify URLs
|
||||
text = text.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||
text = text.replace(/(www\.[^\s]+)/g, '<a href="http://$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||
return text;
|
||||
}
|
||||
|
||||
// Task Details Modal logic
|
||||
const taskDetailsModal = document.getElementById('task-details-modal');
|
||||
const closeTaskDetailsBtn = document.getElementById('close-task-details');
|
||||
const saveTaskChangesBtn = document.getElementById('save-task-changes');
|
||||
const editFieldBtns = document.querySelectorAll('.edit-field-btn');
|
||||
let currentEditingTask = null;
|
||||
|
||||
function openTaskDetailsModal(task) {
|
||||
currentEditingTask = task;
|
||||
// Populate display fields with linkified data
|
||||
document.getElementById('display-description').innerHTML = linkifyText(task.querySelector('p').textContent);
|
||||
document.getElementById('display-stakeholders').innerHTML = linkifyText(task.dataset.stakeholders || '');
|
||||
document.getElementById('display-notes').innerHTML = linkifyText(task.dataset.notes || '');
|
||||
// Populate edit fields with current values
|
||||
document.getElementById('edit-description').value = task.querySelector('p').textContent;
|
||||
document.getElementById('edit-due-date').value = task.dataset.dueDate || '';
|
||||
document.getElementById('edit-started-date').value = task.dataset.startedDate || '';
|
||||
document.getElementById('edit-stakeholders').value = task.dataset.stakeholders || '';
|
||||
document.getElementById('edit-notes').value = task.dataset.notes || '';
|
||||
// Hide all edit fields initially (except dates which are always visible)
|
||||
document.querySelectorAll('[id^="edit-"]:not(#edit-due-date):not(#edit-started-date)').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('[id^="display-"]').forEach(el => el.classList.remove('hidden'));
|
||||
taskDetailsModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTaskDetailsModal() {
|
||||
taskDetailsModal.classList.add('hidden');
|
||||
currentEditingTask = null;
|
||||
}
|
||||
|
||||
function toggleEditMode(field) {
|
||||
const displayEl = document.getElementById(`display-${field}`);
|
||||
const editEl = document.getElementById(`edit-${field}`);
|
||||
if (editEl.classList.contains('hidden')) {
|
||||
// Switch to edit mode
|
||||
editEl.classList.remove('hidden');
|
||||
displayEl.classList.add('hidden');
|
||||
if (field === 'description') {
|
||||
editEl.value = currentEditingTask.querySelector('p').textContent;
|
||||
} else if (field === 'stakeholders') {
|
||||
editEl.value = currentEditingTask.dataset.stakeholders || '';
|
||||
} else if (field === 'notes') {
|
||||
editEl.value = currentEditingTask.dataset.notes || '';
|
||||
}
|
||||
editEl.focus();
|
||||
} else {
|
||||
// Switch back to display mode
|
||||
editEl.classList.add('hidden');
|
||||
displayEl.classList.remove('hidden');
|
||||
// Update display with the edited value
|
||||
if (field === 'description') {
|
||||
displayEl.innerHTML = linkifyText(editEl.value);
|
||||
} else if (field === 'stakeholders') {
|
||||
displayEl.innerHTML = linkifyText(editEl.value);
|
||||
} else if (field === 'notes') {
|
||||
displayEl.innerHTML = linkifyText(editEl.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveTaskChanges() {
|
||||
if (!currentEditingTask) return;
|
||||
// Update task data from edit fields
|
||||
const newDescription = document.getElementById('edit-description').value.trim();
|
||||
const newDueDate = document.getElementById('edit-due-date').value;
|
||||
const newStartedDate = document.getElementById('edit-started-date').value;
|
||||
const newStakeholders = document.getElementById('edit-stakeholders').value.trim();
|
||||
const newNotes = document.getElementById('edit-notes').value.trim();
|
||||
|
||||
// Update task element
|
||||
currentEditingTask.querySelector('p').innerHTML = linkifyText(newDescription);
|
||||
currentEditingTask.dataset.dueDate = newDueDate;
|
||||
currentEditingTask.dataset.startedDate = newStartedDate;
|
||||
currentEditingTask.dataset.stakeholders = newStakeholders;
|
||||
currentEditingTask.dataset.notes = newNotes;
|
||||
|
||||
// Update display spans on task card
|
||||
const dueDateSpan = currentEditingTask.querySelector('.due-date-display');
|
||||
if (dueDateSpan) {
|
||||
if (newDueDate) {
|
||||
const date = new Date(newDueDate);
|
||||
dueDateSpan.textContent = `Due: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||
dueDateSpan.classList.remove('hidden');
|
||||
} else {
|
||||
dueDateSpan.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
const startedDateSpan = currentEditingTask.querySelector('.started-date-display');
|
||||
if (startedDateSpan) {
|
||||
if (newStartedDate) {
|
||||
const date = new Date(newStartedDate);
|
||||
startedDateSpan.textContent = `Started: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||
startedDateSpan.classList.remove('hidden');
|
||||
} else {
|
||||
startedDateSpan.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
saveBoardState();
|
||||
closeTaskDetailsModal();
|
||||
}
|
||||
|
||||
if (closeTaskDetailsBtn) {
|
||||
closeTaskDetailsBtn.addEventListener('click', closeTaskDetailsModal);
|
||||
}
|
||||
if (saveTaskChangesBtn) {
|
||||
saveTaskChangesBtn.addEventListener('click', saveTaskChanges);
|
||||
}
|
||||
editFieldBtns.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const field = e.target.dataset.field;
|
||||
toggleEditMode(field);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal on Escape key
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !taskDetailsModal.classList.contains('hidden')) {
|
||||
closeTaskDetailsModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Function to create a new task element
|
||||
function createNewTask(text, dueDate, stakeholders, notes) {
|
||||
const task = document.createElement('div');
|
||||
task.className = 'task p-4 bg-gray-50 rounded-lg shadow cursor-grab active:cursor-grabbing relative';
|
||||
task.draggable = true;
|
||||
const p = document.createElement('p');
|
||||
p.textContent = text;
|
||||
task.appendChild(p);
|
||||
// Due Date
|
||||
const dueDateSpan = document.createElement('span');
|
||||
dueDateSpan.className = 'due-date-display text-xs text-gray-500 mt-2 block';
|
||||
if (dueDate) {
|
||||
task.dataset.dueDate = dueDate; // Store date for sorting
|
||||
const dateParts = dueDate.split('-');
|
||||
const date = new Date(Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2]));
|
||||
dueDateSpan.textContent = `Due: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||
} else {
|
||||
dueDateSpan.classList.add('hidden');
|
||||
dueDateSpan.textContent = 'Due: --';
|
||||
}
|
||||
task.appendChild(dueDateSpan);
|
||||
// Store stakeholders and notes in dataset
|
||||
if (stakeholders) task.dataset.stakeholders = stakeholders;
|
||||
if (notes) task.dataset.notes = notes;
|
||||
// Started Date
|
||||
const startedDateSpan = document.createElement('span');
|
||||
startedDateSpan.className = 'started-date-display text-xs text-gray-500 mt-1 block hidden';
|
||||
startedDateSpan.textContent = 'Started: --';
|
||||
task.appendChild(startedDateSpan);
|
||||
// Completed Date
|
||||
const completedDateSpan = document.createElement('span');
|
||||
completedDateSpan.className = 'completed-date-display text-xs text-gray-500 mt-1 block hidden';
|
||||
completedDateSpan.textContent = 'Completed: --';
|
||||
task.appendChild(completedDateSpan);
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'delete-task-btn absolute top-2 right-2 text-red-500 hover:text-red-700 hidden';
|
||||
deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>`;
|
||||
task.appendChild(deleteBtn);
|
||||
const tagCircle = document.createElement('div');
|
||||
tagCircle.className = 'tag-circle absolute bottom-2 right-2 w-4 h-4 rounded-full border-2 border-gray-400 cursor-pointer';
|
||||
task.appendChild(tagCircle);
|
||||
const tooltipOverlay = document.createElement('div');
|
||||
tooltipOverlay.className = 'task-tooltip-overlay absolute inset-0 bg-black bg-opacity-60 rounded-lg flex items-center justify-center text-white font-bold hidden pointer-events-none';
|
||||
task.appendChild(tooltipOverlay);
|
||||
initializeTaskEvents(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
// Initialize existing tasks
|
||||
const tasks = document.querySelectorAll('.task');
|
||||
tasks.forEach(initializeTaskEvents);
|
||||
// Event listeners for drop zones (task containers)
|
||||
const taskContainers = document.querySelectorAll('.tasks');
|
||||
const todayDisplayString = new Date().toLocaleDateString(undefined, { timeZone: 'UTC' });
|
||||
const todayDataString = new Date().toISOString().split('T')[0];
|
||||
taskContainers.forEach(container => {
|
||||
container.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
const afterElement = getDragAfterElement(container, e.clientY);
|
||||
const currentlyDragged = document.querySelector('.dragging');
|
||||
if (afterElement == null) {
|
||||
container.appendChild(currentlyDragged);
|
||||
} else {
|
||||
container.insertBefore(currentlyDragged, afterElement);
|
||||
}
|
||||
});
|
||||
container.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
if (!draggedTask) return;
|
||||
const columnId = container.dataset.columnId;
|
||||
const startedDateDisplay = draggedTask.querySelector('.started-date-display');
|
||||
const completedDateDisplay = draggedTask.querySelector('.completed-date-display');
|
||||
const deleteBtn = draggedTask.querySelector('.delete-task-btn');
|
||||
switch (columnId) {
|
||||
case 'inprogress':
|
||||
// Set started date only if it's not already set
|
||||
if (!draggedTask.dataset.startedDate) {
|
||||
draggedTask.dataset.startedDate = todayDataString;
|
||||
if(startedDateDisplay) {
|
||||
startedDateDisplay.textContent = `Started: ${todayDisplayString}`;
|
||||
startedDateDisplay.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
// Clear completed date
|
||||
draggedTask.removeAttribute('data-completed-date');
|
||||
if(completedDateDisplay) {
|
||||
completedDateDisplay.textContent = 'Completed: --';
|
||||
completedDateDisplay.classList.add('hidden');
|
||||
}
|
||||
if (deleteBtn) deleteBtn.classList.add('hidden');
|
||||
break;
|
||||
case 'completed':
|
||||
// Set started date if it's not already set
|
||||
if (!draggedTask.dataset.startedDate) {
|
||||
draggedTask.dataset.startedDate = todayDataString;
|
||||
if(startedDateDisplay) {
|
||||
startedDateDisplay.textContent = `Started: ${todayDisplayString}`;
|
||||
startedDateDisplay.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
// Set completed date
|
||||
draggedTask.dataset.completedDate = todayDataString;
|
||||
if(completedDateDisplay) {
|
||||
completedDateDisplay.textContent = `Completed: ${todayDisplayString}`;
|
||||
completedDateDisplay.classList.remove('hidden');
|
||||
}
|
||||
if (deleteBtn) deleteBtn.classList.remove('hidden');
|
||||
break;
|
||||
case 'todo':
|
||||
// Clear both dates
|
||||
draggedTask.removeAttribute('data-started-date');
|
||||
draggedTask.removeAttribute('data-completed-date');
|
||||
if(startedDateDisplay) {
|
||||
startedDateDisplay.textContent = 'Started: --';
|
||||
startedDateDisplay.classList.add('hidden');
|
||||
}
|
||||
if(completedDateDisplay) {
|
||||
completedDateDisplay.textContent = 'Completed: --';
|
||||
completedDateDisplay.classList.add('hidden');
|
||||
}
|
||||
if (deleteBtn) deleteBtn.classList.add('hidden');
|
||||
break;
|
||||
}
|
||||
draggedTask.dataset.columnId = columnId; // Persist new column for this task
|
||||
saveBoardState(); // Persist after drop
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to determine where to drop the element
|
||||
function getDragAfterElement(container, y) {
|
||||
const draggableElements = [...container.querySelectorAll('.task:not(.dragging)')];
|
||||
return draggableElements.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = y - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) {
|
||||
return { offset: offset, element: child };
|
||||
} else {
|
||||
return closest;
|
||||
}
|
||||
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||
}
|
||||
// Modal functionality
|
||||
const modal = document.getElementById('task-modal');
|
||||
const addTaskButtons = document.querySelectorAll('.add-task-btn');
|
||||
const cancelTaskButton = document.getElementById('cancel-task');
|
||||
const saveTaskButton = document.getElementById('save-task');
|
||||
const taskInput = document.getElementById('task-input');
|
||||
const dueDateInput = document.getElementById('task-due-date');
|
||||
const stakeholdersInput = document.getElementById('stakeholders-input');
|
||||
const notesInput = document.getElementById('notes-input');
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
const cancelDeleteBtn = document.getElementById('cancel-delete');
|
||||
const confirmDeleteBtn = document.getElementById('confirm-delete');
|
||||
addTaskButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
targetColumn = document.querySelector(`#${button.dataset.column} .tasks`);
|
||||
modal.classList.remove('hidden');
|
||||
taskInput.focus();
|
||||
});
|
||||
});
|
||||
function closeModal() {
|
||||
modal.classList.add('hidden');
|
||||
taskInput.value = '';
|
||||
dueDateInput.value = '';
|
||||
if (stakeholdersInput) stakeholdersInput.value = '';
|
||||
if (notesInput) notesInput.value = '';
|
||||
targetColumn = null;
|
||||
}
|
||||
cancelTaskButton.addEventListener('click', closeModal);
|
||||
saveTaskButton.addEventListener('click', () => {
|
||||
const taskText = taskInput.value.trim();
|
||||
const dueDate = dueDateInput.value;
|
||||
const stakeholders = stakeholdersInput ? stakeholdersInput.value.trim() : '';
|
||||
const notes = notesInput ? notesInput.value.trim() : '';
|
||||
if (taskText && targetColumn) {
|
||||
const newTask = createNewTask(taskText, dueDate, stakeholders, notes);
|
||||
targetColumn.appendChild(newTask);
|
||||
saveBoardState(); // Persist new task
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
// Close modal on Escape key
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (!modal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
if (!tagModal.classList.contains('hidden')) {
|
||||
closeTagModal();
|
||||
}
|
||||
if (!deleteModal.classList.contains('hidden')) {
|
||||
deleteModal.classList.add('hidden');
|
||||
taskToDelete = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Delete modal functionality
|
||||
cancelDeleteBtn.addEventListener('click', () => {
|
||||
deleteModal.classList.add('hidden');
|
||||
taskToDelete = null;
|
||||
});
|
||||
confirmDeleteBtn.addEventListener('click', () => {
|
||||
if (taskToDelete) {
|
||||
taskToDelete.remove();
|
||||
saveBoardState(); // Ensure localStorage is updated after deletion
|
||||
}
|
||||
deleteModal.classList.add('hidden');
|
||||
taskToDelete = null;
|
||||
});
|
||||
// Sort functionality
|
||||
const sortButtons = document.querySelectorAll('.sort-btn');
|
||||
sortButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const columnId = button.dataset.columnId;
|
||||
const column = document.getElementById(columnId);
|
||||
const taskContainer = column.querySelector('.tasks');
|
||||
const tasks = Array.from(taskContainer.querySelectorAll('.task'));
|
||||
tasks.sort((a, b) => {
|
||||
const dateA = a.dataset.dueDate ? new Date(a.dataset.dueDate) : null;
|
||||
const dateB = b.dataset.dueDate ? new Date(b.dataset.dueDate) : null;
|
||||
if (dateA && dateB) {
|
||||
return dateA - dateB; // Sort by date ascending
|
||||
}
|
||||
if (dateA) {
|
||||
return -1; // A has a date, B doesn't, so A comes first
|
||||
}
|
||||
if (dateB) {
|
||||
return 1; // B has a date, A doesn't, so B comes first
|
||||
}
|
||||
return 0; // Both have no date, keep original order
|
||||
});
|
||||
// Re-append tasks in sorted order
|
||||
tasks.forEach(task => taskContainer.appendChild(task));
|
||||
});
|
||||
});
|
||||
// Helper to determine text color based on background
|
||||
function getContrastYIQ(hexcolor){
|
||||
hexcolor = hexcolor.replace("#", "");
|
||||
var r = parseInt(hexcolor.substr(0,2),16);
|
||||
var g = parseInt(hexcolor.substr(2,2),16);
|
||||
var b = parseInt(hexcolor.substr(4,2),16);
|
||||
var yiq = ((r*299)+(g*587)+(b*114))/1000;
|
||||
return (yiq >= 128) ? 'black' : 'white';
|
||||
}
|
||||
|
||||
// Tag filter dropdown logic
|
||||
const tagFilter = document.getElementById('tag-filter');
|
||||
const tasksContainers = document.querySelectorAll('.tasks');
|
||||
function getAllTags() {
|
||||
const tags = new Set();
|
||||
document.querySelectorAll('.task').forEach(task => {
|
||||
if (task.dataset.tagName) {
|
||||
tags.add(task.dataset.tagName);
|
||||
}
|
||||
});
|
||||
return Array.from(tags);
|
||||
}
|
||||
function updateTagOptions() {
|
||||
const select = tagFilter;
|
||||
const currentValue = select.value; // Save current selection
|
||||
// Remove all except All and Unassigned
|
||||
select.querySelectorAll('option:not([value="all"]):not([value="unassigned"])').forEach(opt => opt.remove());
|
||||
getAllTags().forEach(tag => {
|
||||
if (!select.querySelector('option[value="' + tag + '"]')) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tag;
|
||||
opt.textContent = tag;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
});
|
||||
select.value = currentValue; // Restore selection
|
||||
}
|
||||
function filterTasks() {
|
||||
const value = tagFilter.value;
|
||||
document.querySelectorAll('.task').forEach(task => {
|
||||
if (value === 'all') {
|
||||
task.style.display = '';
|
||||
} else if (value === 'unassigned') {
|
||||
if (!task.dataset.tagName) {
|
||||
task.style.display = '';
|
||||
} else {
|
||||
task.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
if (task.dataset.tagName === value) {
|
||||
task.style.display = '';
|
||||
} else {
|
||||
task.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
tagFilter.addEventListener('change', filterTasks);
|
||||
// Update tag options on DOM changes
|
||||
setInterval(updateTagOptions, 1000);
|
||||
|
||||
// Save board locally and remotely
|
||||
function saveBoardState() {
|
||||
const save_json = convertBoardToJSON();
|
||||
saveBoardToLocalStorage(save_json);
|
||||
saveBoardToRemoteStorage(save_json);
|
||||
}
|
||||
|
||||
// Convert current board state to JSON
|
||||
function convertBoardToJSON() {
|
||||
const boardData = {
|
||||
tasks: [],
|
||||
tags: {...tags}
|
||||
};
|
||||
document.querySelectorAll('.tasks').forEach(container => {
|
||||
const columnId = container.dataset.columnId;
|
||||
Array.from(container.querySelectorAll('.task')).forEach(task => {
|
||||
boardData.tasks.push({
|
||||
text: task.querySelector('p').textContent,
|
||||
dueDate: task.dataset.dueDate || '',
|
||||
startedDate: task.dataset.startedDate || '',
|
||||
completedDate: task.dataset.completedDate || '',
|
||||
tagName: task.dataset.tagName || '',
|
||||
tagColor: task.dataset.tagColor || '',
|
||||
stakeholders: task.dataset.stakeholders || '',
|
||||
notes: task.dataset.notes || '',
|
||||
column: columnId
|
||||
});
|
||||
});
|
||||
});
|
||||
return JSON.stringify(boardData);
|
||||
}
|
||||
|
||||
// Local Storage Persistence
|
||||
function saveBoardToLocalStorage(save_json) {
|
||||
localStorage.setItem('kanbanBoard', save_json);
|
||||
}
|
||||
|
||||
// RemoteStorage Persistence
|
||||
function saveBoardToRemoteStorage(save_json) {
|
||||
// Placeholder for Post to RESTFUL endpoint, etc...
|
||||
console.log('Saving board to remote storage not yet implemented:', save_json);
|
||||
}
|
||||
|
||||
// Load Board From Remote Storage
|
||||
function loadBoardFromRemoteStorage(input_json) {
|
||||
convertJSONToBoard(input_json);
|
||||
}
|
||||
|
||||
// Load board state from local storage
|
||||
function loadBoardFromLocalStorage() {
|
||||
const data = localStorage.getItem('kanbanBoard');
|
||||
if (!data) return;
|
||||
convertJSONToBoard(data);
|
||||
}
|
||||
|
||||
// Convert JSON back to board state
|
||||
function convertJSONToBoard(json) {
|
||||
let boardData;
|
||||
try {
|
||||
boardData = JSON.parse(json);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse kanbanBoard JSON:', e);
|
||||
return;
|
||||
}
|
||||
// Clear all columns
|
||||
document.querySelectorAll('.tasks').forEach(container => container.innerHTML = '');
|
||||
// Restore tags
|
||||
Object.keys(tags).forEach(k => delete tags[k]);
|
||||
Object.assign(tags, boardData.tags);
|
||||
// Restore tasks
|
||||
boardData.tasks.forEach(taskData => {
|
||||
const task = createNewTask(taskData.text, taskData.dueDate, taskData.stakeholders, taskData.notes);
|
||||
if (taskData.startedDate) task.dataset.startedDate = taskData.startedDate;
|
||||
if (taskData.completedDate) task.dataset.completedDate = taskData.completedDate;
|
||||
if (taskData.tagName) {
|
||||
task.dataset.tagName = taskData.tagName;
|
||||
task.dataset.tagColor = taskData.tagColor;
|
||||
const tagCircle = task.querySelector('.tag-circle');
|
||||
if (tagCircle) {
|
||||
tagCircle.style.backgroundColor = taskData.tagColor;
|
||||
tagCircle.classList.remove('border-2', 'border-gray-400');
|
||||
}
|
||||
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||
if (tooltipOverlay) {
|
||||
tooltipOverlay.textContent = taskData.tagName;
|
||||
}
|
||||
}
|
||||
// Set started/completed date display
|
||||
const startedDateDisplay = task.querySelector('.started-date-display');
|
||||
if (startedDateDisplay && taskData.startedDate) {
|
||||
startedDateDisplay.textContent = `Started: ${new Date(taskData.startedDate).toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||
startedDateDisplay.classList.remove('hidden');
|
||||
}
|
||||
const completedDateDisplay = task.querySelector('.completed-date-display');
|
||||
if (completedDateDisplay && taskData.completedDate) {
|
||||
completedDateDisplay.textContent = `Completed: ${new Date(taskData.completedDate).toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||
completedDateDisplay.classList.remove('hidden');
|
||||
}
|
||||
// Show/hide delete button
|
||||
const deleteBtn = task.querySelector('.delete-task-btn');
|
||||
if (deleteBtn) {
|
||||
if (taskData.column === 'completed') {
|
||||
deleteBtn.classList.remove('hidden');
|
||||
} else {
|
||||
deleteBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
// Append to correct column
|
||||
const column = document.querySelector(`.tasks[data-column-id="${taskData.column}"]`);
|
||||
if (column) column.appendChild(task);
|
||||
});
|
||||
}
|
||||
|
||||
// Call load on startup
|
||||
loadBoardFromLocalStorage();
|
||||
|
||||
// Save after every UI change
|
||||
function saveAfter(fn) {
|
||||
return function(...args) {
|
||||
const result = fn.apply(this, args);
|
||||
saveBoardState();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
// Patch UI actions to save
|
||||
const originalCreateNewTask = createNewTask;
|
||||
createNewTask = function(...args) {
|
||||
const task = originalCreateNewTask.apply(this, args);
|
||||
saveBoardState();
|
||||
return task;
|
||||
};
|
||||
const originalApplyTagToTask = applyTagToTask;
|
||||
applyTagToTask = function(...args) {
|
||||
originalApplyTagToTask.apply(this, args);
|
||||
saveBoardState();
|
||||
};
|
||||
const originalRemoveTagFromTask = removeTagFromTask;
|
||||
removeTagFromTask = function(...args) {
|
||||
originalRemoveTagFromTask.apply(this, args);
|
||||
saveBoardState();
|
||||
};
|
||||
const originalOpenTagModal = openTagModal;
|
||||
openTagModal = function(...args) {
|
||||
originalOpenTagModal.apply(this, args);
|
||||
saveBoardState();
|
||||
};
|
||||
|
||||
// Dark mode toggle logic
|
||||
function setupDarkModeToggle() {
|
||||
const darkToggle = document.getElementById('dark-mode-toggle');
|
||||
const dot = document.querySelector('.dot');
|
||||
const mainBody = document.getElementById('main-body');
|
||||
|
||||
// Check for saved preference or system preference
|
||||
const savedMode = localStorage.getItem('darkMode');
|
||||
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Set initial state
|
||||
if (savedMode === 'true' || (!savedMode && systemPrefersDark)) {
|
||||
darkToggle.checked = true;
|
||||
mainBody.classList.add('dark');
|
||||
if (dot) dot.style.transform = 'translateX(16px)';
|
||||
} else {
|
||||
darkToggle.checked = false;
|
||||
mainBody.classList.remove('dark');
|
||||
if (dot) dot.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
darkToggle.addEventListener('change', function() {
|
||||
if (darkToggle.checked) {
|
||||
mainBody.classList.add('dark');
|
||||
localStorage.setItem('darkMode', 'true');
|
||||
if (dot) dot.style.transform = 'translateX(16px)';
|
||||
} else {
|
||||
mainBody.classList.remove('dark');
|
||||
localStorage.setItem('darkMode', 'false');
|
||||
if (dot) dot.style.transform = 'translateX(0)';
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for system preference changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
// Only auto-switch if user hasn't manually set a preference
|
||||
if (!localStorage.getItem('darkMode')) {
|
||||
if (e.matches) {
|
||||
darkToggle.checked = true;
|
||||
mainBody.classList.add('dark');
|
||||
if (dot) dot.style.transform = 'translateX(16px)';
|
||||
} else {
|
||||
darkToggle.checked = false;
|
||||
mainBody.classList.remove('dark');
|
||||
if (dot) dot.style.transform = 'translateX(0)';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dark mode
|
||||
setupDarkModeToggle();
|
||||
});
|
||||
BIN
tools/kanban/src/kanban.ico
Normal file
BIN
tools/kanban/src/kanban.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
162
tools/kanban/src/styles.css
Normal file
162
tools/kanban/src/styles.css
Normal file
@@ -0,0 +1,162 @@
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
body.dark {
|
||||
background-color: #18181b;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.kanban-column {
|
||||
user-select: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
body.dark .kanban-column {
|
||||
background-color: #23232a;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.task {
|
||||
background-color: #f9fafb;
|
||||
color: #222;
|
||||
word-break: break-word;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
body.dark .task {
|
||||
background-color: #262632;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.tasks::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.tasks::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
body.dark .tasks::-webkit-scrollbar-track {
|
||||
background: #23232a;
|
||||
}
|
||||
.tasks::-webkit-scrollbar-thumb {
|
||||
background: #94a3b8;
|
||||
border-radius: 10px;
|
||||
}
|
||||
body.dark .tasks::-webkit-scrollbar-thumb {
|
||||
background: #444459;
|
||||
}
|
||||
.tasks::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
body.dark .tasks::-webkit-scrollbar-thumb:hover {
|
||||
background: #6366f1;
|
||||
}
|
||||
.tag-tooltip {
|
||||
visibility: hidden;
|
||||
width: 120px;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
margin-left: -60px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
body.dark .tag-tooltip {
|
||||
background-color: #222;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.tag-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #555 transparent transparent transparent;
|
||||
}
|
||||
body.dark .tag-tooltip::after {
|
||||
border-color: #222 transparent transparent transparent;
|
||||
}
|
||||
.tag-circle:hover .tag-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
.dot {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
#dark-mode-toggle:checked + span .dot {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
#dark-mode-toggle:not(:checked) + span .dot {
|
||||
transform: translateX(0);
|
||||
}
|
||||
body.dark h1,
|
||||
body.dark h2,
|
||||
body.dark h3,
|
||||
body.dark h4,
|
||||
body.dark h5,
|
||||
body.dark h6,
|
||||
body.dark p,
|
||||
body.dark .text-gray-700,
|
||||
body.dark .text-gray-900,
|
||||
body.dark .text-gray-600,
|
||||
body.dark .font-medium,
|
||||
body.dark .font-bold {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
body.dark .bg-white {
|
||||
background-color: #23232a !important;
|
||||
}
|
||||
body.dark .bg-gray-50 {
|
||||
background-color: #262632 !important;
|
||||
}
|
||||
body.dark .border-gray-400 {
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
body.dark select#tag-filter {
|
||||
background-color: #23232a !important;
|
||||
color: #e5e7eb !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
body.dark select#tag-filter option {
|
||||
background-color: #23232a !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
body.dark .modal,
|
||||
body.dark #delete-modal > div,
|
||||
body.dark #tag-delete-modal > div {
|
||||
background-color: #23232a !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
body.dark #delete-modal h3,
|
||||
body.dark #tag-delete-modal h3,
|
||||
body.dark #delete-modal p,
|
||||
body.dark #tag-delete-modal p {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
body.dark #delete-modal button,
|
||||
body.dark #tag-delete-modal button {
|
||||
background-color: #444459 !important;
|
||||
color: #e5e7eb !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
body.dark #delete-modal button:hover,
|
||||
body.dark #tag-delete-modal button:hover {
|
||||
background-color: #6366f1 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
body.dark input[type="text"],
|
||||
body.dark input[type="date"],
|
||||
body.dark textarea {
|
||||
background-color: #23232a !important;
|
||||
color: #e5e7eb !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
body.dark input[type="text"]::placeholder,
|
||||
body.dark input[type="date"]::placeholder,
|
||||
body.dark textarea::placeholder {
|
||||
color: #a1a1aa !important;
|
||||
}
|
||||
Reference in New Issue
Block a user