From c310b22e8e80c9ce73b90630040baae4a20ab9ef Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Wed, 4 Mar 2026 18:09:34 -0800 Subject: [PATCH] feat: add drag-and-drop task reordering (#14, #15, #16) --- public/app.js | 73 +++++++++++++++++++++++++++++++++++++++++++++-- public/styles.css | 25 ++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/public/app.js b/public/app.js index 89a631f..5659b26 100644 --- a/public/app.js +++ b/public/app.js @@ -270,7 +270,8 @@ function renderBoard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'Done' }), }); - loadTasks(); + initDragAndDrop(); + loadTasks(); }); cardsEl.appendChild(cardEl); @@ -328,11 +329,13 @@ function initTasksPage() { }); e.target.reset(); - loadTasks(); + initDragAndDrop(); + loadTasks(); }); initTaskFilters(); populateAgentDropdown(); + initDragAndDrop(); loadTasks(); } @@ -1375,3 +1378,69 @@ function renderGiteaActivity() { `).join(''); } + +// ============ DRAG AND DROP ============ +let draggedTask = null; + +function initDragAndDrop() { + const board = document.getElementById('board'); + if (!board) return; + + // Add drag listeners to columns + document.querySelectorAll('.column').forEach(column => { + column.addEventListener('dragover', handleDragOver); + column.addEventListener('drop', handleDrop); + column.addEventListener('dragleave', handleDragLeave); + }); +} + +function handleDragOver(e) { + e.preventDefault(); + e.currentTarget.classList.add('drag-over'); +} + +function handleDragLeave(e) { + e.currentTarget.classList.remove('drag-over'); +} + +async function handleDrop(e) { + e.preventDefault(); + const column = e.currentTarget; + column.classList.remove('drag-over'); + + if (!draggedTask) return; + + const newStatus = column.dataset.status; + if (newStatus === draggedTask.status) return; + + // Update task status via API + try { + await fetch(`/api/tasks/${draggedTask.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) + }); + + // Reload tasks + await initDragAndDrop(); + loadTasks(); + } catch (err) { + console.error('Failed to update task:', err); + } + + draggedTask = null; +} + +function makeTaskCardDraggable(cardEl, task) { + cardEl.draggable = true; + + cardEl.addEventListener('dragstart', (e) => { + draggedTask = task; + cardEl.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + }); + + cardEl.addEventListener('dragend', () => { + cardEl.classList.remove('dragging'); + }); +} diff --git a/public/styles.css b/public/styles.css index bca0ed1..525222d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1000,3 +1000,28 @@ label { flex: 1; } } + +/* Drag and Drop */ +.task-card { + cursor: grab; + transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s; +} + +.task-card:active { + cursor: grabbing; +} + +.task-card.dragging { + opacity: 0.5; + transform: scale(1.02); +} + +.column.drag-over { + background: var(--hover-bg); + border: 2px dashed var(--primary); +} + +.column.drag-over .column-header { + background: var(--primary); + color: white; +}