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 = `
Click the "+ New Goal" button to create your first learning goal đ
Unable to load your goals. Please check if the server is running.