const STATUSES = ['Backlog', 'Todo', 'In Progress', 'Review', 'Done']; const board = document.getElementById('board'); const template = document.getElementById('task-template'); const form = document.getElementById('task-form'); let tasks = []; function renderBoard() { board.innerHTML = ''; for (const status of STATUSES) { const column = document.createElement('section'); column.className = 'column'; const heading = document.createElement('h2'); heading.textContent = `${status} (${tasks.filter((t) => t.status === status).length})`; const cards = document.createElement('div'); cards.className = 'cards'; tasks .filter((task) => task.status === status) .forEach((task) => cards.appendChild(renderCard(task))); column.appendChild(heading); column.appendChild(cards); board.appendChild(column); } } function renderCard(task) { const node = template.content.firstElementChild.cloneNode(true); node.querySelector('.card-title').textContent = task.title; node.querySelector('.card-desc').textContent = task.description || 'No description'; node.querySelector('.assignee').textContent = `Assignee: ${task.assignee || 'Unassigned'}`; node.querySelector('.tags').textContent = `Tags: ${(task.tags || []).join(', ') || 'None'}`; const badge = node.querySelector('.priority'); badge.textContent = task.priority; badge.classList.add((task.priority || '').toLowerCase()); const statusSelect = node.querySelector('.status-select'); statusSelect.value = task.status; statusSelect.addEventListener('change', async () => { await updateTask(task.id, { status: statusSelect.value }); }); return node; } async function loadTasks() { const res = await fetch('/api/tasks'); tasks = await res.json(); renderBoard(); } async function createTask(payload) { const res = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'failed_to_create_task'); } return res.json(); } async function updateTask(id, payload) { const res = await fetch(`/api/tasks/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'failed_to_update_task'); } return res.json(); } form.addEventListener('submit', async (e) => { e.preventDefault(); const payload = { title: form.title.value, description: form.description.value, assignee: form.assignee.value, priority: form.priority.value, status: form.status.value, tags: form.tags.value .split(',') .map((s) => s.trim()) .filter(Boolean), }; try { await createTask(payload); form.reset(); form.priority.value = 'Medium'; form.status.value = 'Backlog'; } catch (err) { alert(err.message); } }); function upsertTask(task) { const idx = tasks.findIndex((t) => t.id === task.id); if (idx === -1) { tasks.unshift(task); } else { tasks[idx] = task; } renderBoard(); } function connectWebSocket() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${proto}//${location.host}`); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'task_created' || msg.type === 'task_updated') { upsertTask(msg.payload); } }; ws.onclose = () => { setTimeout(connectWebSocket, 1200); }; } loadTasks(); connectWebSocket();