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 = `
+
+
+ ${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 = `
+
+
+ ${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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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