const API_URL = '/api'; class Task { constructor(id, title, description, size, reward) { this.id = id; this.title = title; this.description = description; this.size = size; this.reward = reward; this.completed = false; this.subtasks = []; } getProgressWeight() { const weights = { small: 10, medium: 25, large: 50 }; return weights[this.size] || 25; } calculateProgress() { if (this.subtasks.length === 0) { return this.completed ? 100 : 0; } const totalWeight = this.subtasks.reduce((sum, subtask) => sum + subtask.getProgressWeight(), 0); const completedWeight = this.subtasks.reduce((sum, subtask) => { return sum + (subtask.completed ? subtask.getProgressWeight() : 0); }, 0); return totalWeight > 0 ? Math.round((completedWeight / totalWeight) * 100) : 0; } addSubtask(subtask) { this.subtasks.push(subtask); } removeSubtask(subtaskId) { this.subtasks = this.subtasks.filter(st => st.id !== subtaskId); } toggleComplete() { this.completed = !this.completed; if (this.completed && this.subtasks.length > 0) { this.subtasks.forEach(st => st.completed = true); } } } class App { constructor() { this.tasks = []; this.draggedTask = null; this.draggedSubtask = null; this.draggedParent = null; this.init(); } async init() { this.setupEventListeners(); await this.loadTasks(); } setupEventListeners() { const form = document.getElementById('task-form'); const openSidebar = document.getElementById('open-sidebar'); const closeSidebar = document.getElementById('close-sidebar'); const overlay = document.getElementById('overlay'); form.addEventListener('submit', (e) => this.handleSubmit(e)); openSidebar.addEventListener('click', () => this.toggleSidebar(true)); closeSidebar.addEventListener('click', () => this.toggleSidebar(false)); overlay.addEventListener('click', () => this.toggleSidebar(false)); } toggleSidebar(open) { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('overlay'); if (open) { sidebar.classList.add('open'); overlay.classList.add('active'); } else { sidebar.classList.remove('open'); overlay.classList.remove('active'); } } async handleSubmit(e) { e.preventDefault(); const title = document.getElementById('task-title').value; const description = document.getElementById('task-description').value; const size = document.getElementById('task-size').value; const reward = document.getElementById('task-reward').value; try { const response = await fetch(`${API_URL}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description, size, reward }) }); if (response.ok) { await this.loadTasks(); e.target.reset(); this.toggleSidebar(false); } else { alert('Failed to create task'); } } catch (error) { console.error('Error creating task:', error); alert('Failed to create task'); } } async loadTasks() { try { const response = await fetch(`${API_URL}/tasks`); const data = await response.json(); this.tasks = data.map(taskData => { const task = new Task( taskData.id, taskData.title, taskData.description, taskData.size, taskData.reward ); task.completed = taskData.completed; if (taskData.subtasks) { task.subtasks = taskData.subtasks.map(stData => { const subtask = new Task( stData.id, stData.title, stData.description, stData.size, stData.reward ); subtask.completed = stData.completed; return subtask; }); } return task; }); this.render(); } catch (error) { console.error('Error loading tasks:', error); this.showError(); } } async deleteTask(taskId) { if (!confirm('Are you sure you want to delete this task?')) { return; } try { const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'DELETE' }); if (response.ok) { await this.loadTasks(); } else { alert('Failed to delete task'); } } catch (error) { console.error('Error deleting task:', error); alert('Failed to delete task'); } } async toggleTaskComplete(taskId) { const task = this.tasks.find(t => t.id === taskId); if (!task) return; task.toggleComplete(); try { const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task) }); if (response.ok) { await this.loadTasks(); } else { alert('Failed to update task'); } } catch (error) { console.error('Error updating task:', error); alert('Failed to update task'); } } async toggleSubtaskComplete(taskId, subtaskId) { const task = this.tasks.find(t => t.id === taskId); if (!task) return; const subtask = task.subtasks.find(st => st.id === subtaskId); if (!subtask) return; subtask.completed = !subtask.completed; try { const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task) }); if (response.ok) { await this.loadTasks(); } else { alert('Failed to update subtask'); } } catch (error) { console.error('Error updating subtask:', error); alert('Failed to update subtask'); } } async deleteSubtask(taskId, subtaskId) { const task = this.tasks.find(t => t.id === taskId); if (!task) return; task.removeSubtask(subtaskId); try { const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task) }); if (response.ok) { await this.loadTasks(); } else { alert('Failed to delete subtask'); } } catch (error) { console.error('Error deleting subtask:', error); alert('Failed to delete subtask'); } } handleDragStart(e, task, isSubtask = false, parentTask = null) { if (isSubtask) { this.draggedSubtask = task; this.draggedParent = parentTask; } else { this.draggedTask = task; } e.target.classList.add('dragging'); } handleDragEnd(e) { e.target.classList.remove('dragging'); this.draggedTask = null; this.draggedSubtask = null; this.draggedParent = null; document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over'); }); } handleDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('drag-over'); } handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); } async handleDrop(e, targetTask) { e.preventDefault(); e.currentTarget.classList.remove('drag-over'); if (this.draggedTask && this.draggedTask.id !== targetTask.id) { targetTask.addSubtask(this.draggedTask); this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id); try { await fetch(`${API_URL}/tasks/${this.draggedTask.id}`, { method: 'DELETE' }); await fetch(`${API_URL}/tasks/${targetTask.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(targetTask) }); await this.loadTasks(); } catch (error) { console.error('Error moving task:', error); alert('Failed to move task'); } } else if (this.draggedSubtask && this.draggedParent) { if (targetTask.id !== this.draggedParent.id) { this.draggedParent.removeSubtask(this.draggedSubtask.id); targetTask.addSubtask(this.draggedSubtask); try { await fetch(`${API_URL}/tasks/${this.draggedParent.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.draggedParent) }); await fetch(`${API_URL}/tasks/${targetTask.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(targetTask) }); await this.loadTasks(); } catch (error) { console.error('Error moving subtask:', error); alert('Failed to move subtask'); } } } } async handleTaskReorder(e, targetIndex) { e.preventDefault(); e.currentTarget.classList.remove('drag-over'); if (this.draggedTask) { const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id); if (currentIndex !== -1 && currentIndex !== targetIndex) { this.tasks.splice(currentIndex, 1); this.tasks.splice(targetIndex, 0, this.draggedTask); try { await fetch(`${API_URL}/tasks/reorder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.tasks.map(t => t.id)) }); await this.loadTasks(); } catch (error) { console.error('Error reordering tasks:', error); } } } } renderTask(task, index) { const progress = task.calculateProgress(); const isCompleted = progress === 100 || task.completed; const taskCard = document.createElement('div'); taskCard.className = `task-card ${isCompleted ? 'completed' : ''}`; taskCard.draggable = true; taskCard.addEventListener('dragstart', (e) => this.handleDragStart(e, task)); taskCard.addEventListener('dragend', (e) => this.handleDragEnd(e)); taskCard.addEventListener('dragover', (e) => this.handleDragOver(e)); taskCard.addEventListener('dragleave', (e) => this.handleDragLeave(e)); taskCard.addEventListener('drop', (e) => { if (this.draggedTask && this.draggedTask.id !== task.id) { this.handleDrop(e, task); } else { this.handleTaskReorder(e, index); } }); taskCard.innerHTML = `
${this.escapeHtml(task.title)}
${task.size}
${task.description ? `
${this.escapeHtml(task.description)}
` : ''}
Progress ${progress}%
${progress > 20 ? progress + '%' : ''}
${task.reward ? `
🎁 Reward: ${isCompleted ? `${this.escapeHtml(task.reward)}` : `Complete to reveal your reward!` }
` : ''}
${task.subtasks.length > 0 ? this.renderSubtasks(task) : ''}
đŸ“Ĩ Drop tasks here to create subtasks
`; return taskCard; } renderSubtasks(task) { const container = document.createElement('div'); container.className = 'subtasks-container'; task.subtasks.forEach(subtask => { const subtaskDiv = document.createElement('div'); subtaskDiv.className = 'subtask'; subtaskDiv.draggable = true; subtaskDiv.addEventListener('dragstart', (e) => this.handleDragStart(e, subtask, true, task)); subtaskDiv.addEventListener('dragend', (e) => this.handleDragEnd(e)); const progress = subtask.calculateProgress(); subtaskDiv.innerHTML = `
${this.escapeHtml(subtask.title)}
${subtask.size}
${subtask.description ? `
${this.escapeHtml(subtask.description)}
` : ''}
${subtask.reward && subtask.completed ? `
🎁 ${this.escapeHtml(subtask.reward)}
` : ''} `; container.appendChild(subtaskDiv); }); return container.outerHTML; } render() { const container = document.getElementById('tasks-container'); container.innerHTML = ''; if (this.tasks.length === 0) { container.innerHTML = `

No goals yet!

Click the "+ New Goal" button to create your first learning goal 🚀

`; return; } this.tasks.forEach((task, index) => { container.appendChild(this.renderTask(task, index)); }); } showError() { const container = document.getElementById('tasks-container'); container.innerHTML = `

âš ī¸ Connection Error

Unable to load your goals. Please check if the server is running.

`; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } const app = new App();