diff --git a/dockerifle b/dockerifle new file mode 100644 index 0000000..9948c44 --- /dev/null +++ b/dockerifle @@ -0,0 +1,26 @@ +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install --production + +# Copy application files +COPY public ./public +COPY server ./server + +# Create directory for SQLite database +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3000 + +# Set environment variable for database location +ENV DATABASE_PATH=/app/data/tasks.db + +# Start the application +CMD ["node", "server/server.js"] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6434c10 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "learning-progress-tracker", + "version": "1.0.0", + "description": "A learning progress tracker with server-side storage", + "main": "server/server.js", + "scripts": { + "start": "node server/server.js", + "dev": "nodemon server/server.js" + }, + "keywords": [ + "learning", + "tracker", + "progress", + "goals" + ], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6", + "body-parser": "^1.20.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..8049e31 --- /dev/null +++ b/public/app.js @@ -0,0 +1,511 @@ +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(); \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..7f46d49 --- /dev/null +++ b/public/index.html @@ -0,0 +1,67 @@ + + + + + + Learning Progress Tracker + + + + + + + +
+ + +
+
+

📚 Learning Progress Tracker

+

Track your learning goals and celebrate your achievements!

+ +
+ +
+
+
+

Loading your goals...

+
+
+
+ + + + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..435ad2b --- /dev/null +++ b/public/styles.css @@ -0,0 +1,498 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 30px; + position: relative; +} + +h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); +} + +header p { + font-size: 1.1em; + margin-bottom: 20px; +} + +/* Sidebar Styles */ +.sidebar { + position: fixed; + top: 0; + right: -450px; + width: 450px; + height: 100vh; + background: white; + box-shadow: -5px 0 20px rgba(0,0,0,0.3); + transition: right 0.3s ease; + z-index: 1001; + overflow-y: auto; +} + +.sidebar.open { + right: 0; +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 25px; + border-bottom: 2px solid #f0f0f0; + position: sticky; + top: 0; + background: white; + z-index: 10; +} + +.sidebar-header h2 { + color: #333; + font-size: 1.5em; +} + +.close-btn { + background: none; + border: none; + font-size: 2em; + color: #999; + cursor: pointer; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s; +} + +.close-btn:hover { + background: #f0f0f0; + color: #333; +} + +.sidebar-content { + padding: 25px; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; +} + +.overlay.active { + opacity: 1; + visibility: visible; +} + +/* Form Styles */ +.form-group { + margin-bottom: 20px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #333; + font-size: 0.95em; +} + +input[type="text"], +select, +textarea { + width: 100%; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.3s; +} + +input[type="text"]:focus, +select:focus, +textarea:focus { + outline: none; + border-color: #667eea; +} + +textarea { + resize: vertical; + min-height: 80px; +} + +/* Button Styles */ +button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +button:active { + transform: translateY(0); +} + +.btn-primary { + width: 100%; + padding: 15px; + font-size: 1.1em; +} + +.btn-add-task { + margin-top: 15px; + font-size: 1.1em; + padding: 12px 40px; +} + +.btn-small { + padding: 8px 15px; + font-size: 14px; +} + +.btn-complete { + background: #28a745; +} + +.btn-delete { + background: #dc3545; +} + +.btn-complete:hover { + background: #218838; +} + +.btn-delete:hover { + background: #c82333; +} + +/* Tasks Container */ +.tasks-container { + display: grid; + gap: 20px; +} + +.task-card { + background: white; + border-radius: 15px; + padding: 20px; + box-shadow: 0 5px 20px rgba(0,0,0,0.1); + cursor: move; + transition: transform 0.2s, box-shadow 0.2s; +} + +.task-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(0,0,0,0.15); +} + +.task-card.dragging { + opacity: 0.5; +} + +.task-card.drag-over { + border: 2px dashed #667eea; + background: #f8f9ff; +} + +.task-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; +} + +.task-title { + font-size: 1.3em; + font-weight: 700; + color: #333; + flex: 1; +} + +.task-size-badge { + padding: 5px 12px; + border-radius: 20px; + font-size: 0.8em; + font-weight: 600; + margin-left: 10px; +} + +.task-size-badge.small { + background: #e3f2fd; + color: #1976d2; +} + +.task-size-badge.medium { + background: #fff3e0; + color: #f57c00; +} + +.task-size-badge.large { + background: #fce4ec; + color: #c2185b; +} + +.task-description { + color: #666; + margin-bottom: 15px; + line-height: 1.5; +} + +/* Progress Bar */ +.progress-section { + margin-bottom: 15px; +} + +.progress-label { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 0.9em; + color: #666; +} + +.progress-bar { + width: 100%; + height: 25px; + background: #f0f0f0; + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + font-size: 0.85em; +} + +/* Reward Section */ +.reward-section { + background: #f8f9fa; + padding: 12px; + border-radius: 8px; + margin-top: 15px; +} + +.reward-hidden { + color: #999; + font-style: italic; +} + +.reward-revealed { + color: #28a745; + font-weight: 600; + animation: reveal 0.5s ease; +} + +@keyframes reveal { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Task Actions */ +.task-actions { + display: flex; + gap: 10px; + margin-top: 15px; + flex-wrap: wrap; +} + +/* Subtasks */ +.subtasks-container { + margin-top: 15px; + padding-left: 20px; + border-left: 3px solid #e0e0e0; +} + +.subtask { + background: #f8f9fa; + padding: 12px; + border-radius: 8px; + margin-bottom: 10px; + cursor: move; +} + +.subtask-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.subtask-title { + font-weight: 600; + color: #333; +} + +.subtask-progress { + width: 100%; + height: 20px; + background: #e0e0e0; + border-radius: 10px; + overflow: hidden; + margin-bottom: 8px; +} + +.subtask-progress-fill { + height: 100%; + background: linear-gradient(90deg, #4caf50 0%, #81c784 100%); + transition: width 0.3s ease; +} + +/* Drop Zone */ +.drop-zone { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 15px; + text-align: center; + color: #999; + margin-top: 10px; + transition: all 0.3s; +} + +.drop-zone.drag-over { + border-color: #667eea; + background: #f0f4ff; + color: #667eea; +} + +/* Completed State */ +.completed { + opacity: 0.7; +} + +.completed .task-title { + text-decoration: line-through; +} + +/* Checkbox Group */ +.checkbox-group { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 15px; +} + +.checkbox-group input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: white; +} + +.empty-state h2 { + font-size: 2em; + margin-bottom: 10px; +} + +.empty-state p { + font-size: 1.2em; + opacity: 0.9; +} + +/* Loading State */ +.loading { + text-align: center; + padding: 60px 20px; + color: white; +} + +.spinner { + border: 4px solid rgba(255,255,255,0.3); + border-top: 4px solid white; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .sidebar { + width: 100%; + right: -100%; + } + + h1 { + font-size: 2em; + } + + .task-header { + flex-direction: column; + } + + .task-size-badge { + margin-left: 0; + margin-top: 5px; + } + + .task-actions { + flex-direction: column; + } + + .task-actions button { + width: 100%; + } +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..a97db08 --- /dev/null +++ b/server.js @@ -0,0 +1,196 @@ +const express = require('express'); +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const bodyParser = require('body-parser'); +const fs = require('fs'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(bodyParser.json()); +app.use(express.static(path.join(__dirname, '../public'))); + +// Ensure data directory exists +const dataDir = path.join(__dirname, '../data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +// Initialize SQLite database +const dbPath = process.env.DATABASE_PATH || path.join(dataDir, 'tasks.db'); +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('Error opening database:', err); + } else { + console.log('Connected to SQLite database at:', dbPath); + initDatabase(); + } +}); + +function initDatabase() { + db.run(` + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + size TEXT NOT NULL, + reward TEXT, + completed INTEGER DEFAULT 0, + position INTEGER, + subtasks TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); +} + +// API Routes + +// Get all tasks +app.get('/api/tasks', (req, res) => { + db.all('SELECT * FROM tasks ORDER BY position ASC, created_at ASC', [], (err, rows) => { + if (err) { + console.error('Error fetching tasks:', err); + return res.status(500).json({ error: 'Failed to fetch tasks' }); + } + + const tasks = rows.map(row => ({ + id: row.id, + title: row.title, + description: row.description, + size: row.size, + reward: row.reward, + completed: row.completed === 1, + subtasks: row.subtasks ? JSON.parse(row.subtasks) : [] + })); + + res.json(tasks); + }); +}); + +// Create a new task +app.post('/api/tasks', (req, res) => { + const { title, description, size, reward } = req.body; + const id = Date.now().toString(); + + db.run( + `INSERT INTO tasks (id, title, description, size, reward, position) + VALUES (?, ?, ?, ?, ?, (SELECT COALESCE(MAX(position), 0) + 1 FROM tasks))`, + [id, title, description || '', size, reward || ''], + function(err) { + if (err) { + console.error('Error creating task:', err); + return res.status(500).json({ error: 'Failed to create task' }); + } + + res.json({ + id, + title, + description: description || '', + size, + reward: reward || '', + completed: false, + subtasks: [] + }); + } + ); +}); + +// Update a task +app.put('/api/tasks/:id', (req, res) => { + const { id } = req.params; + const { title, description, size, reward, completed, subtasks } = req.body; + + db.run( + `UPDATE tasks + SET title = ?, description = ?, size = ?, reward = ?, completed = ?, subtasks = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [ + title, + description || '', + size, + reward || '', + completed ? 1 : 0, + JSON.stringify(subtasks || []), + id + ], + function(err) { + if (err) { + console.error('Error updating task:', err); + return res.status(500).json({ error: 'Failed to update task' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json({ success: true }); + } + ); +}); + +// Delete a task +app.delete('/api/tasks/:id', (req, res) => { + const { id } = req.params; + + db.run('DELETE FROM tasks WHERE id = ?', [id], function(err) { + if (err) { + console.error('Error deleting task:', err); + return res.status(500).json({ error: 'Failed to delete task' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json({ success: true }); + }); +}); + +// Reorder tasks +app.post('/api/tasks/reorder', (req, res) => { + const taskIds = req.body; + + if (!Array.isArray(taskIds)) { + return res.status(400).json({ error: 'Invalid task order data' }); + } + + const promises = taskIds.map((id, index) => { + return new Promise((resolve, reject) => { + db.run('UPDATE tasks SET position = ? WHERE id = ?', [index, id], (err) => { + if (err) reject(err); + else resolve(); + }); + }); + }); + + Promise.all(promises) + .then(() => res.json({ success: true })) + .catch(err => { + console.error('Error reordering tasks:', err); + res.status(500).json({ error: 'Failed to reorder tasks' }); + }); +}); + +// Serve the main HTML file +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../public/index.html')); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 Learning Progress Tracker running on http://localhost:${PORT}`); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + db.close((err) => { + if (err) { + console.error('Error closing database:', err); + } else { + console.log('Database connection closed'); + } + process.exit(0); + }); +}); \ No newline at end of file