latest version
This commit is contained in:
1 parent
907c293a68
commit
fa169b880c
5 files changed
+765
-457
No files matched your search
File renamed without changes.
+370
-216
@@ -12,11 +12,7 @@ class Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getProgressWeight() {
|
getProgressWeight() {
|
||||||
const weights = {
|
const weights = { small: 10, medium: 25, large: 50 };
|
||||||
small: 10,
|
|
||||||
medium: 25,
|
|
||||||
large: 50
|
|
||||||
};
|
|
||||||
return weights[this.size] || 25;
|
return weights[this.size] || 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,28 +21,13 @@ class Task {
|
|||||||
return this.completed ? 100 : 0;
|
return this.completed ? 100 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalWeight = this.subtasks.reduce((sum, subtask) => sum + subtask.getProgressWeight(), 0);
|
const totalWeight = this.subtasks.reduce((sum, st) => sum + st.getProgressWeight(), 0);
|
||||||
const completedWeight = this.subtasks.reduce((sum, subtask) => {
|
const completedWeight = this.subtasks.reduce((sum, st) => {
|
||||||
return sum + (subtask.completed ? subtask.getProgressWeight() : 0);
|
return sum + (st.completed ? st.getProgressWeight() : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return totalWeight > 0 ? Math.round((completedWeight / totalWeight) * 100) : 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 {
|
class App {
|
||||||
@@ -55,6 +36,11 @@ class App {
|
|||||||
this.draggedTask = null;
|
this.draggedTask = null;
|
||||||
this.draggedSubtask = null;
|
this.draggedSubtask = null;
|
||||||
this.draggedParent = null;
|
this.draggedParent = null;
|
||||||
|
this.draggedSubtaskIndex = null;
|
||||||
|
this.dropZoneTimer = null;
|
||||||
|
this.contextMenuTarget = null;
|
||||||
|
this.contextMenuType = null;
|
||||||
|
this.contextMenuParent = null;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,37 +51,190 @@ class App {
|
|||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
const form = document.getElementById('task-form');
|
const form = document.getElementById('task-form');
|
||||||
const openSidebar = document.getElementById('open-sidebar');
|
const openModal = document.getElementById('open-modal');
|
||||||
const closeSidebar = document.getElementById('close-sidebar');
|
const closeModal = document.getElementById('close-modal');
|
||||||
const overlay = document.getElementById('overlay');
|
const modal = document.getElementById('modal');
|
||||||
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||||
openSidebar.addEventListener('click', () => this.toggleSidebar(true));
|
openModal.addEventListener('click', () => this.openCreateModal());
|
||||||
closeSidebar.addEventListener('click', () => this.toggleSidebar(false));
|
closeModal.addEventListener('click', () => this.toggleModal(false));
|
||||||
overlay.addEventListener('click', () => this.toggleSidebar(false));
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) this.toggleModal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('context-edit').addEventListener('click', () => this.handleContextEdit());
|
||||||
|
document.getElementById('context-delete').addEventListener('click', () => this.handleContextDelete());
|
||||||
|
document.getElementById('context-promote').addEventListener('click', () => this.handleContextPromote());
|
||||||
|
|
||||||
|
document.addEventListener('click', () => this.hideContextMenu());
|
||||||
|
contextMenu.addEventListener('click', (e) => e.stopPropagation());
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar(open) {
|
openCreateModal() {
|
||||||
const sidebar = document.getElementById('sidebar');
|
document.getElementById('modal-title').textContent = 'Create New Goal';
|
||||||
const overlay = document.getElementById('overlay');
|
document.getElementById('submit-btn').textContent = 'Add Goal';
|
||||||
|
document.getElementById('task-form').reset();
|
||||||
|
document.getElementById('edit-task-id').value = '';
|
||||||
|
document.getElementById('edit-parent-id').value = '';
|
||||||
|
this.toggleModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
openEditModal(task, parentTask = null) {
|
||||||
|
document.getElementById('modal-title').textContent = parentTask ? 'Edit Subtask' : 'Edit Goal';
|
||||||
|
document.getElementById('submit-btn').textContent = 'Update';
|
||||||
|
document.getElementById('edit-task-id').value = task.id;
|
||||||
|
document.getElementById('edit-parent-id').value = parentTask ? parentTask.id : '';
|
||||||
|
document.getElementById('task-title').value = task.title;
|
||||||
|
document.getElementById('task-description').value = task.description || '';
|
||||||
|
document.getElementById('task-size').value = task.size;
|
||||||
|
document.getElementById('task-reward').value = task.reward || '';
|
||||||
|
|
||||||
|
this.toggleModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleModal(open) {
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
if (open) {
|
if (open) {
|
||||||
sidebar.classList.add('open');
|
modal.classList.add('open');
|
||||||
overlay.classList.add('active');
|
|
||||||
} else {
|
} else {
|
||||||
sidebar.classList.remove('open');
|
modal.classList.remove('open');
|
||||||
overlay.classList.remove('active');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showContextMenu(e, target, type, parentTask = null) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
const promoteOption = document.getElementById('context-promote');
|
||||||
|
|
||||||
|
this.contextMenuTarget = target;
|
||||||
|
this.contextMenuType = type;
|
||||||
|
this.contextMenuParent = parentTask;
|
||||||
|
|
||||||
|
// Show "Promote to Task" only for subtasks
|
||||||
|
if (type === 'subtask') {
|
||||||
|
promoteOption.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
promoteOption.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenu.style.left = e.pageX + 'px';
|
||||||
|
contextMenu.style.top = e.pageY + 'px';
|
||||||
|
contextMenu.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideContextMenu() {
|
||||||
|
document.getElementById('context-menu').classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextEdit() {
|
||||||
|
if (this.contextMenuTarget) {
|
||||||
|
this.openEditModal(this.contextMenuTarget, this.contextMenuParent);
|
||||||
|
}
|
||||||
|
this.hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleContextDelete() {
|
||||||
|
if (!this.contextMenuTarget) return;
|
||||||
|
|
||||||
|
if (!confirm('Delete this ' + (this.contextMenuType === 'task' ? 'task' : 'subtask') + '?')) {
|
||||||
|
this.hideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.contextMenuType === 'task') {
|
||||||
|
await this.deleteTask(this.contextMenuTarget.id);
|
||||||
|
} else if (this.contextMenuType === 'subtask' && this.contextMenuParent) {
|
||||||
|
await this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleContextPromote() {
|
||||||
|
if (!this.contextMenuTarget || this.contextMenuType !== 'subtask' || !this.contextMenuParent) {
|
||||||
|
this.hideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from parent's subtasks
|
||||||
|
const parentTask = this.tasks.find(t => t.id === this.contextMenuParent.id);
|
||||||
|
if (!parentTask) {
|
||||||
|
this.hideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtask = this.contextMenuTarget;
|
||||||
|
parentTask.subtasks = parentTask.subtasks.filter(st => st.id !== subtask.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update parent task
|
||||||
|
await fetch(`${API_URL}/tasks/${parentTask.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(parentTask)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create new top-level task
|
||||||
|
await fetch(`${API_URL}/tasks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: subtask.title,
|
||||||
|
description: subtask.description,
|
||||||
|
size: subtask.size,
|
||||||
|
reward: subtask.reward
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadTasks();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error promoting subtask:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
async handleSubmit(e) {
|
async handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const title = document.getElementById('task-title').value;
|
const title = document.getElementById('task-title').value.trim();
|
||||||
const description = document.getElementById('task-description').value;
|
const description = document.getElementById('task-description').value.trim();
|
||||||
const size = document.getElementById('task-size').value;
|
const size = document.getElementById('task-size').value;
|
||||||
const reward = document.getElementById('task-reward').value;
|
const reward = document.getElementById('task-reward').value.trim();
|
||||||
|
const editId = document.getElementById('edit-task-id').value;
|
||||||
|
const parentId = document.getElementById('edit-parent-id').value;
|
||||||
|
|
||||||
|
if (editId) {
|
||||||
|
if (parentId) {
|
||||||
|
// Editing a subtask
|
||||||
|
const parentTask = this.tasks.find(t => t.id === parentId);
|
||||||
|
if (parentTask) {
|
||||||
|
const subtask = parentTask.subtasks.find(st => st.id === editId);
|
||||||
|
if (subtask) {
|
||||||
|
subtask.title = title;
|
||||||
|
subtask.description = description;
|
||||||
|
subtask.size = size;
|
||||||
|
subtask.reward = reward;
|
||||||
|
await this.updateTask(parentTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Editing a main task
|
||||||
|
const task = this.tasks.find(t => t.id === editId);
|
||||||
|
if (task) {
|
||||||
|
task.title = title;
|
||||||
|
task.description = description;
|
||||||
|
task.size = size;
|
||||||
|
task.reward = reward;
|
||||||
|
await this.updateTask(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Creating new task
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/tasks`, {
|
const response = await fetch(`${API_URL}/tasks`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -103,43 +242,28 @@ class App {
|
|||||||
body: JSON.stringify({ title, description, size, reward })
|
body: JSON.stringify({ title, description, size, reward })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) await this.loadTasks();
|
||||||
await this.loadTasks();
|
|
||||||
e.target.reset();
|
|
||||||
this.toggleSidebar(false);
|
|
||||||
} else {
|
|
||||||
alert('Failed to create task');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating task:', error);
|
console.error('Error creating task:', error);
|
||||||
alert('Failed to create task');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.target.reset();
|
||||||
|
this.toggleModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
async loadTasks() {
|
async loadTasks() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/tasks`);
|
const response = await fetch(`${API_URL}/tasks`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
this.tasks = data.map(taskData => {
|
this.tasks = data.map(taskData => {
|
||||||
const task = new Task(
|
const task = new Task(taskData.id, taskData.title, taskData.description, taskData.size, taskData.reward);
|
||||||
taskData.id,
|
|
||||||
taskData.title,
|
|
||||||
taskData.description,
|
|
||||||
taskData.size,
|
|
||||||
taskData.reward
|
|
||||||
);
|
|
||||||
task.completed = taskData.completed;
|
task.completed = taskData.completed;
|
||||||
|
|
||||||
if (taskData.subtasks) {
|
if (taskData.subtasks && Array.isArray(taskData.subtasks)) {
|
||||||
task.subtasks = taskData.subtasks.map(stData => {
|
task.subtasks = taskData.subtasks.map(stData => {
|
||||||
const subtask = new Task(
|
const subtask = new Task(stData.id, stData.title, stData.description, stData.size, stData.reward);
|
||||||
stData.id,
|
|
||||||
stData.title,
|
|
||||||
stData.description,
|
|
||||||
stData.size,
|
|
||||||
stData.reward
|
|
||||||
);
|
|
||||||
subtask.completed = stData.completed;
|
subtask.completed = stData.completed;
|
||||||
return subtask;
|
return subtask;
|
||||||
});
|
});
|
||||||
@@ -148,6 +272,7 @@ class App {
|
|||||||
return task;
|
return task;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateOverallProgress();
|
||||||
this.render();
|
this.render();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading tasks:', error);
|
console.error('Error loading tasks:', error);
|
||||||
@@ -155,24 +280,37 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTask(taskId) {
|
updateOverallProgress() {
|
||||||
if (!confirm('Are you sure you want to delete this task?')) {
|
const totalTasks = this.tasks.length;
|
||||||
|
const completedTasks = this.tasks.filter(t => t.calculateProgress() === 100).length;
|
||||||
|
|
||||||
|
document.getElementById('completed-count').textContent = completedTasks;
|
||||||
|
document.getElementById('total-count').textContent = totalTasks;
|
||||||
|
|
||||||
|
if (totalTasks === 0) {
|
||||||
|
document.getElementById('overall-percentage').textContent = '0%';
|
||||||
|
document.getElementById('overall-progress-fill').style.width = '0%';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const totalWeight = this.tasks.reduce((sum, t) => sum + t.getProgressWeight(), 0);
|
||||||
const response = await fetch(`${API_URL}/tasks/${taskId}`, {
|
const completedWeight = this.tasks.reduce((sum, t) => {
|
||||||
method: 'DELETE'
|
const progress = t.calculateProgress();
|
||||||
});
|
return sum + (t.getProgressWeight() * progress / 100);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
if (response.ok) {
|
const overallProgress = totalWeight > 0 ? Math.round((completedWeight / totalWeight) * 100) : 0;
|
||||||
await this.loadTasks();
|
|
||||||
} else {
|
document.getElementById('overall-percentage').textContent = overallProgress + '%';
|
||||||
alert('Failed to delete task');
|
document.getElementById('overall-progress-fill').style.width = overallProgress + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteTask(taskId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) await this.loadTasks();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting task:', error);
|
console.error('Error deleting task:', error);
|
||||||
alert('Failed to delete task');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,24 +318,14 @@ class App {
|
|||||||
const task = this.tasks.find(t => t.id === taskId);
|
const task = this.tasks.find(t => t.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
task.toggleComplete();
|
task.completed = !task.completed;
|
||||||
|
|
||||||
try {
|
// When completing or uncompleting a task, also update all subtasks
|
||||||
const response = await fetch(`${API_URL}/tasks/${taskId}`, {
|
if (task.subtasks.length > 0) {
|
||||||
method: 'PUT',
|
task.subtasks.forEach(st => st.completed = task.completed);
|
||||||
headers: { 'Content-Type': 'application/json' },
|
}
|
||||||
body: JSON.stringify(task)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
await this.updateTask(task);
|
||||||
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) {
|
async toggleSubtaskComplete(taskId, subtaskId) {
|
||||||
@@ -208,72 +336,64 @@ class App {
|
|||||||
if (!subtask) return;
|
if (!subtask) return;
|
||||||
|
|
||||||
subtask.completed = !subtask.completed;
|
subtask.completed = !subtask.completed;
|
||||||
|
await this.updateTask(task);
|
||||||
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) {
|
async deleteSubtask(taskId, subtaskId) {
|
||||||
const task = this.tasks.find(t => t.id === taskId);
|
const task = this.tasks.find(t => t.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
task.removeSubtask(subtaskId);
|
task.subtasks = task.subtasks.filter(st => st.id !== subtaskId);
|
||||||
|
await this.updateTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(task) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/tasks/${taskId}`, {
|
const response = await fetch(`${API_URL}/tasks/${task.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(task)
|
body: JSON.stringify(task)
|
||||||
});
|
});
|
||||||
|
if (response.ok) await this.loadTasks();
|
||||||
if (response.ok) {
|
|
||||||
await this.loadTasks();
|
|
||||||
} else {
|
|
||||||
alert('Failed to delete subtask');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting subtask:', error);
|
console.error('Error updating task:', error);
|
||||||
alert('Failed to delete subtask');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragStart(e, task, isSubtask = false, parentTask = null) {
|
handleDragStart(e, item, isSubtask = false, parentTask = null, index = null) {
|
||||||
if (isSubtask) {
|
if (isSubtask) {
|
||||||
this.draggedSubtask = task;
|
this.draggedSubtask = item;
|
||||||
this.draggedParent = parentTask;
|
this.draggedParent = parentTask;
|
||||||
|
this.draggedSubtaskIndex = index;
|
||||||
|
this.draggedTask = null;
|
||||||
} else {
|
} else {
|
||||||
this.draggedTask = task;
|
this.draggedTask = item;
|
||||||
|
this.draggedSubtask = null;
|
||||||
|
this.draggedParent = null;
|
||||||
|
this.draggedSubtaskIndex = null;
|
||||||
}
|
}
|
||||||
e.target.classList.add('dragging');
|
e.currentTarget.classList.add('dragging');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnd(e) {
|
handleDragEnd(e) {
|
||||||
e.target.classList.remove('dragging');
|
e.currentTarget.classList.remove('dragging');
|
||||||
|
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||||
|
document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('visible'));
|
||||||
|
|
||||||
|
if (this.dropZoneTimer) {
|
||||||
|
clearTimeout(this.dropZoneTimer);
|
||||||
|
this.dropZoneTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.draggedTask = null;
|
this.draggedTask = null;
|
||||||
this.draggedSubtask = null;
|
this.draggedSubtask = null;
|
||||||
this.draggedParent = null;
|
this.draggedParent = null;
|
||||||
|
this.draggedSubtaskIndex = null;
|
||||||
document.querySelectorAll('.drag-over').forEach(el => {
|
|
||||||
el.classList.remove('drag-over');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragOver(e) {
|
handleCardDragOver(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
e.currentTarget.classList.add('drag-over');
|
e.currentTarget.classList.add('drag-over');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,58 +401,58 @@ class App {
|
|||||||
e.currentTarget.classList.remove('drag-over');
|
e.currentTarget.classList.remove('drag-over');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e, taskId) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.draggedTask && !this.draggedSubtask) return;
|
||||||
|
|
||||||
|
if (this.dropZoneTimer) clearTimeout(this.dropZoneTimer);
|
||||||
|
|
||||||
|
this.dropZoneTimer = setTimeout(() => {
|
||||||
|
const dropZone = document.querySelector(`[data-task-id="${taskId}"] .drop-zone`);
|
||||||
|
if (dropZone && (this.draggedTask || this.draggedSubtask)) {
|
||||||
|
dropZone.classList.add('visible');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
async handleDrop(e, targetTask) {
|
async handleDrop(e, targetTask) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
e.currentTarget.classList.remove('drag-over');
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
|
||||||
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
|
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
|
||||||
targetTask.addSubtask(this.draggedTask);
|
// Move task as subtask
|
||||||
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
|
targetTask.subtasks.push(this.draggedTask);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_URL}/tasks/${this.draggedTask.id}`, { method: 'DELETE' });
|
await fetch(`${API_URL}/tasks/${this.draggedTask.id}`, { method: 'DELETE' });
|
||||||
await fetch(`${API_URL}/tasks/${targetTask.id}`, {
|
await this.updateTask(targetTask);
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(targetTask)
|
|
||||||
});
|
|
||||||
await this.loadTasks();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving task:', error);
|
console.error('Error moving task:', error);
|
||||||
alert('Failed to move task');
|
|
||||||
}
|
}
|
||||||
} else if (this.draggedSubtask && this.draggedParent) {
|
} else if (this.draggedSubtask && this.draggedParent && targetTask.id !== this.draggedParent.id) {
|
||||||
if (targetTask.id !== this.draggedParent.id) {
|
// Move subtask between different parent tasks
|
||||||
this.draggedParent.removeSubtask(this.draggedSubtask.id);
|
this.draggedParent.subtasks = this.draggedParent.subtasks.filter(st => st.id !== this.draggedSubtask.id);
|
||||||
targetTask.addSubtask(this.draggedSubtask);
|
targetTask.subtasks.push(this.draggedSubtask);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_URL}/tasks/${this.draggedParent.id}`, {
|
await this.updateTask(this.draggedParent);
|
||||||
method: 'PUT',
|
await this.updateTask(targetTask);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error moving subtask:', error);
|
console.error('Error moving subtask:', error);
|
||||||
alert('Failed to move subtask');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleTaskReorder(e, targetIndex) {
|
async handleTaskReorder(e, targetIndex) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
e.currentTarget.classList.remove('drag-over');
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
|
||||||
if (this.draggedTask) {
|
if (!this.draggedTask) return;
|
||||||
|
|
||||||
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
|
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
|
||||||
if (currentIndex !== -1 && currentIndex !== targetIndex) {
|
if (currentIndex === -1 || currentIndex === targetIndex) return;
|
||||||
|
|
||||||
this.tasks.splice(currentIndex, 1);
|
this.tasks.splice(currentIndex, 1);
|
||||||
this.tasks.splice(targetIndex, 0, this.draggedTask);
|
this.tasks.splice(targetIndex, 0, this.draggedTask);
|
||||||
|
|
||||||
@@ -342,42 +462,65 @@ class App {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(this.tasks.map(t => t.id))
|
body: JSON.stringify(this.tasks.map(t => t.id))
|
||||||
});
|
});
|
||||||
await this.loadTasks();
|
this.render();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reordering tasks:', error);
|
console.error('Error reordering:', error);
|
||||||
}
|
await this.loadTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSubtaskReorder(e, parentTask, targetIndex) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
|
||||||
|
if (!this.draggedSubtask || !this.draggedParent) return;
|
||||||
|
if (this.draggedParent.id !== parentTask.id) return;
|
||||||
|
if (this.draggedSubtaskIndex === targetIndex) return;
|
||||||
|
|
||||||
|
// Reorder within same parent
|
||||||
|
const currentIndex = this.draggedSubtaskIndex;
|
||||||
|
const subtask = parentTask.subtasks[currentIndex];
|
||||||
|
|
||||||
|
parentTask.subtasks.splice(currentIndex, 1);
|
||||||
|
parentTask.subtasks.splice(targetIndex, 0, subtask);
|
||||||
|
|
||||||
|
await this.updateTask(parentTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTask(task, index) {
|
renderTask(task, index) {
|
||||||
const progress = task.calculateProgress();
|
const progress = task.calculateProgress();
|
||||||
const isCompleted = progress === 100 || task.completed;
|
const isCompleted = progress === 100 || task.completed;
|
||||||
|
|
||||||
const taskCard = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
taskCard.className = `task-card ${isCompleted ? 'completed' : ''}`;
|
card.className = `task-card ${isCompleted ? 'completed' : ''}`;
|
||||||
taskCard.draggable = true;
|
card.draggable = true;
|
||||||
|
card.setAttribute('data-task-id', task.id);
|
||||||
|
|
||||||
taskCard.addEventListener('dragstart', (e) => this.handleDragStart(e, task));
|
card.addEventListener('dragstart', (e) => this.handleDragStart(e, task, false));
|
||||||
taskCard.addEventListener('dragend', (e) => this.handleDragEnd(e));
|
card.addEventListener('dragend', (e) => this.handleDragEnd(e));
|
||||||
taskCard.addEventListener('dragover', (e) => this.handleDragOver(e));
|
card.addEventListener('dragover', (e) => this.handleCardDragOver(e));
|
||||||
taskCard.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
card.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||||
taskCard.addEventListener('drop', (e) => {
|
card.addEventListener('dragenter', (e) => this.handleDragEnter(e, task.id));
|
||||||
if (this.draggedTask && this.draggedTask.id !== task.id) {
|
card.addEventListener('drop', (e) => {
|
||||||
|
if (e.target.closest('.drop-zone')) {
|
||||||
this.handleDrop(e, task);
|
this.handleDrop(e, task);
|
||||||
} else {
|
} else if (this.draggedTask && this.draggedTask.id !== task.id) {
|
||||||
this.handleTaskReorder(e, index);
|
this.handleTaskReorder(e, index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
card.addEventListener('contextmenu', (e) => {
|
||||||
|
if (!e.target.closest('.subtask')) {
|
||||||
|
this.showContextMenu(e, task, 'task');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
taskCard.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
<div class="task-title">${this.escapeHtml(task.title)}</div>
|
<div class="task-title">${this.escapeHtml(task.title)}</div>
|
||||||
<div class="task-size-badge ${task.size}">${task.size}</div>
|
<div class="task-size-badge ${task.size}">${task.size}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${task.description ? `<div class="task-description">${this.escapeHtml(task.description)}</div>` : ''}
|
${task.description ? `<div class="task-description">${this.escapeHtml(task.description)}</div>` : ''}
|
||||||
|
|
||||||
<div class="progress-section">
|
<div class="progress-section">
|
||||||
<div class="progress-label">
|
<div class="progress-label">
|
||||||
<span>Progress</span>
|
<span>Progress</span>
|
||||||
@@ -385,88 +528,99 @@ class App {
|
|||||||
</div>
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" style="width: ${progress}%">
|
<div class="progress-fill" style="width: ${progress}%">
|
||||||
${progress > 20 ? progress + '%' : ''}
|
${progress > 15 ? progress + '%' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${task.reward ? `
|
${task.reward ? `
|
||||||
<div class="reward-section">
|
<div class="reward-section">
|
||||||
<strong>🎁 Reward:</strong>
|
<strong>🎁 Reward:</strong>
|
||||||
${isCompleted
|
${isCompleted
|
||||||
? `<span class="reward-revealed">${this.escapeHtml(task.reward)}</span>`
|
? `<span class="reward-revealed">${this.escapeHtml(task.reward)}</span>`
|
||||||
: `<span class="reward-hidden">Complete to reveal your reward!</span>`
|
: `<span class="reward-hidden">Complete to reveal</span>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<div class="task-actions">
|
<div class="task-actions">
|
||||||
<button class="btn-small btn-complete" onclick="app.toggleTaskComplete('${task.id}')">
|
<button class="btn-small btn-complete" onclick="app.toggleTaskComplete('${task.id}')">
|
||||||
${task.completed ? '↩️ Uncomplete' : '✓ Complete'}
|
${task.completed ? 'Undo' : 'Complete'}
|
||||||
</button>
|
|
||||||
<button class="btn-small btn-delete" onclick="app.deleteTask('${task.id}')">
|
|
||||||
🗑️ Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${task.subtasks.length > 0 ? this.renderSubtasks(task) : ''}
|
${task.subtasks.length > 0 ? this.renderSubtasks(task) : ''}
|
||||||
|
<div class="drop-zone">Drop tasks here to add as subtasks</div>
|
||||||
<div class="drop-zone"
|
|
||||||
ondragover="event.preventDefault(); event.currentTarget.classList.add('drag-over')"
|
|
||||||
ondragleave="event.currentTarget.classList.remove('drag-over')"
|
|
||||||
ondrop="app.handleDrop(event, app.tasks.find(t => t.id === '${task.id}'))">
|
|
||||||
📥 Drop tasks here to create subtasks
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return taskCard;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSubtasks(task) {
|
renderSubtasks(task) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'subtasks-container';
|
container.className = 'subtasks-container';
|
||||||
|
|
||||||
task.subtasks.forEach(subtask => {
|
task.subtasks.forEach((subtask, index) => {
|
||||||
const subtaskDiv = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
subtaskDiv.className = 'subtask';
|
div.className = 'subtask';
|
||||||
subtaskDiv.draggable = true;
|
div.draggable = true;
|
||||||
|
div.setAttribute('data-subtask-id', subtask.id);
|
||||||
|
|
||||||
subtaskDiv.addEventListener('dragstart', (e) => this.handleDragStart(e, subtask, true, task));
|
div.addEventListener('dragstart', (e) => {
|
||||||
subtaskDiv.addEventListener('dragend', (e) => this.handleDragEnd(e));
|
e.stopPropagation();
|
||||||
|
this.handleDragStart(e, subtask, true, task, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('dragend', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleDragEnd(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.draggedSubtask && this.draggedParent && this.draggedParent.id === task.id) {
|
||||||
|
e.currentTarget.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('dragleave', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('drop', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleSubtaskReorder(e, task, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener('contextmenu', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.showContextMenu(e, subtask, 'subtask', task);
|
||||||
|
});
|
||||||
|
|
||||||
const progress = subtask.calculateProgress();
|
const progress = subtask.calculateProgress();
|
||||||
|
|
||||||
subtaskDiv.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="subtask-header">
|
<div class="subtask-header">
|
||||||
<div class="subtask-title">${this.escapeHtml(subtask.title)}</div>
|
<div class="subtask-title">${this.escapeHtml(subtask.title)}</div>
|
||||||
<div class="task-size-badge ${subtask.size}">${subtask.size}</div>
|
<div class="task-size-badge ${subtask.size}">${subtask.size}</div>
|
||||||
</div>
|
</div>
|
||||||
|
${subtask.description ? `<div class="task-description">${this.escapeHtml(subtask.description)}</div>` : ''}
|
||||||
${subtask.description ? `<div class="task-description" style="font-size: 0.9em; margin-bottom: 8px;">${this.escapeHtml(subtask.description)}</div>` : ''}
|
|
||||||
|
|
||||||
<div class="subtask-progress">
|
<div class="subtask-progress">
|
||||||
<div class="subtask-progress-fill" style="width: ${progress}%"></div>
|
<div class="subtask-progress-fill" style="width: ${progress}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="font-size: 0.85em; color: #a0aec0; margin-bottom: 8px;">${progress}%</div>
|
||||||
<div class="checkbox-group">
|
|
||||||
<input type="checkbox" ${subtask.completed ? 'checked' : ''}
|
|
||||||
onchange="app.toggleSubtaskComplete('${task.id}', '${subtask.id}')">
|
|
||||||
<label>${progress}% complete</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${subtask.reward && subtask.completed ? `
|
${subtask.reward && subtask.completed ? `
|
||||||
<div style="font-size: 0.85em; color: #28a745; margin-top: 5px;">
|
<div style="font-size: 0.9em; color: #68d391; margin-bottom: 8px;">
|
||||||
🎁 ${this.escapeHtml(subtask.reward)}
|
🎁 ${this.escapeHtml(subtask.reward)}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
<div class="subtask-actions">
|
||||||
<button class="btn-small btn-delete" style="margin-top: 8px;"
|
<button class="btn-small btn-complete" onclick="event.stopPropagation(); app.toggleSubtaskComplete('${task.id}', '${subtask.id}')">
|
||||||
onclick="app.deleteSubtask('${task.id}', '${subtask.id}')">
|
${subtask.completed ? 'Undo' : 'Complete'}
|
||||||
Remove Subtask
|
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(subtaskDiv);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
return container.outerHTML;
|
return container.outerHTML;
|
||||||
@@ -479,8 +633,8 @@ class App {
|
|||||||
if (this.tasks.length === 0) {
|
if (this.tasks.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h2>No goals yet!</h2>
|
<h2>No goals yet</h2>
|
||||||
<p>Click the "+ New Goal" button to create your first learning goal 🚀</p>
|
<p>Click "+ New Goal" to get started</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -495,8 +649,8 @@ class App {
|
|||||||
const container = document.getElementById('tasks-container');
|
const container = document.getElementById('tasks-container');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h2>⚠️ Connection Error</h2>
|
<h2>Connection Error</h2>
|
||||||
<p>Unable to load your goals. Please check if the server is running.</p>
|
<p>Cannot connect to server</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-21
@@ -7,14 +7,25 @@
|
|||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Sidebar for creating tasks -->
|
<!-- Context Menu -->
|
||||||
<div class="sidebar" id="sidebar">
|
<div class="context-menu" id="context-menu">
|
||||||
<div class="sidebar-header">
|
<div class="context-menu-item" id="context-edit">✏️ Edit</div>
|
||||||
<h2>Create New Goal</h2>
|
<div class="context-menu-item" id="context-promote" style="display: none;">⬆️ Promote to Task</div>
|
||||||
<button class="close-btn" id="close-sidebar">×</button>
|
<div class="context-menu-item danger" id="context-delete">🗑️ Delete</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="task-form" class="sidebar-content">
|
<!-- Modal for creating/editing tasks -->
|
||||||
|
<div class="modal" id="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="modal-title">Create New Goal</h2>
|
||||||
|
<button class="close-btn" id="close-modal">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="task-form">
|
||||||
|
<input type="hidden" id="edit-task-id">
|
||||||
|
<input type="hidden" id="edit-parent-id">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="task-title">Goal Title *</label>
|
<label for="task-title">Goal Title *</label>
|
||||||
<input type="text" id="task-title" placeholder="e.g., Learn JavaScript" required>
|
<input type="text" id="task-title" placeholder="e.g., Learn JavaScript" required>
|
||||||
@@ -22,42 +33,55 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="task-description">Description</label>
|
<label for="task-description">Description</label>
|
||||||
<textarea id="task-description" placeholder="What do you want to achieve?"></textarea>
|
<textarea id="task-description" placeholder="What do you want to achieve?" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="task-size">Goal Size *</label>
|
<label for="task-size">Size *</label>
|
||||||
<select id="task-size" required>
|
<select id="task-size" required>
|
||||||
<option value="small">Small Goal (10% progress weight)</option>
|
<option value="small">Small (10%)</option>
|
||||||
<option value="medium" selected>Medium Goal (25% progress weight)</option>
|
<option value="medium" selected>Medium (25%)</option>
|
||||||
<option value="large">Large Goal (50% progress weight)</option>
|
<option value="large">Large (50%)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="task-reward">Reward (Optional)</label>
|
<label for="task-reward">Reward</label>
|
||||||
<input type="text" id="task-reward" placeholder="e.g., Buy a new book">
|
<input type="text" id="task-reward" placeholder="Optional">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary">Add Goal</button>
|
<button type="submit" class="btn-primary" id="submit-btn">Add Goal</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Overlay -->
|
|
||||||
<div class="overlay" id="overlay"></div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>📚 Learning Progress Tracker</h1>
|
<h1>Learning Progress Tracker</h1>
|
||||||
<p>Track your learning goals and celebrate your achievements!</p>
|
<button class="btn-add" id="open-modal">+ New Goal</button>
|
||||||
<button class="btn-add-task" id="open-sidebar">+ New Goal</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Overall Progress - Always Visible -->
|
||||||
|
<div class="overall-progress" id="overall-progress">
|
||||||
|
<div class="overall-header">
|
||||||
|
<span>Overall Progress</span>
|
||||||
|
<span id="overall-percentage">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="overall-progress-fill" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="task-counter" id="task-counter">
|
||||||
|
<span id="completed-count">0</span> / <span id="total-count">0</span> tasks completed
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tasks-container" id="tasks-container">
|
<div class="tasks-container" id="tasks-container">
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<p>Loading your goals...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+350
-220
@@ -5,427 +5,556 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #0f1419;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
color: #e4e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
margin-bottom: 30px;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 25px;
|
|
||||||
border-bottom: 2px solid #f0f0f0;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: white;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h2 {
|
h1 {
|
||||||
color: #333;
|
font-size: 1.8em;
|
||||||
font-size: 1.5em;
|
color: #e4e6eb;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
/* Context Menu */
|
||||||
background: none;
|
.context-menu {
|
||||||
border: none;
|
position: fixed;
|
||||||
font-size: 2em;
|
background: #1e2936;
|
||||||
color: #999;
|
border: 1px solid #2d3748;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 2000;
|
||||||
|
min-width: 150px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
padding: 10px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 40px;
|
transition: background 0.2s;
|
||||||
height: 40px;
|
font-size: 0.9em;
|
||||||
|
border-bottom: 1px solid #2d3748;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8px;
|
||||||
border-radius: 50%;
|
color: #e4e6eb;
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn:hover {
|
.context-menu-item:last-child {
|
||||||
background: #f0f0f0;
|
border-bottom: none;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.context-menu-item:hover {
|
||||||
padding: 25px;
|
background: #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.context-menu-item.danger:hover {
|
||||||
|
background: #742a2a;
|
||||||
|
color: #feb2b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0,0,0,0.5);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay.active {
|
.modal.open {
|
||||||
opacity: 1;
|
display: flex;
|
||||||
visibility: visible;
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #1a2332;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e4e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8em;
|
||||||
|
color: #8b96a5;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e4e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content form {
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Styles */
|
/* Form Styles */
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #a0aec0;
|
||||||
font-size: 0.95em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 10px 12px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 1px solid #2d3748;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 0.95em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: border-color 0.3s;
|
transition: all 0.2s;
|
||||||
|
background: #0f1419;
|
||||||
|
color: #e4e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #4299e1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
min-height: 80px;
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
background: #1a2332;
|
||||||
|
color: #e4e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button Styles */
|
/* Button Styles */
|
||||||
button {
|
button {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
font-family: inherit;
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
border: none;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
button:hover {
|
transition: all 0.2s;
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
padding: 12px;
|
||||||
font-size: 1.1em;
|
font-size: 1em;
|
||||||
|
background: #4299e1;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-task {
|
.btn-primary:hover {
|
||||||
margin-top: 15px;
|
background: #3182ce;
|
||||||
font-size: 1.1em;
|
}
|
||||||
padding: 12px 40px;
|
|
||||||
|
.btn-add {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
background: #4299e1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add:hover {
|
||||||
|
background: #3182ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small {
|
.btn-small {
|
||||||
padding: 8px 15px;
|
padding: 6px 12px;
|
||||||
font-size: 14px;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-complete {
|
.btn-complete {
|
||||||
background: #28a745;
|
background: #48bb78;
|
||||||
}
|
color: white;
|
||||||
|
|
||||||
.btn-delete {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-complete:hover {
|
.btn-complete:hover {
|
||||||
background: #218838;
|
background: #38a169;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete:hover {
|
.btn-delete:hover {
|
||||||
background: #c82333;
|
background: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background: #4299e1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit:hover {
|
||||||
|
background: #3182ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overall Progress */
|
||||||
|
.overall-progress {
|
||||||
|
background: #1a2332;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e4e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overall-percentage {
|
||||||
|
color: #4299e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-counter {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #a0aec0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#completed-count {
|
||||||
|
color: #4299e1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-counter {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #a0aec0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-counter span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e4e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tasks Container */
|
/* Tasks Container */
|
||||||
.tasks-container {
|
.tasks-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card {
|
.task-card {
|
||||||
background: white;
|
background: #1a2332;
|
||||||
border-radius: 15px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
cursor: move;
|
cursor: grab;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: all 0.2s;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card:hover {
|
.task-card:hover {
|
||||||
transform: translateY(-3px);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
border-color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card.dragging {
|
.task-card.dragging {
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card.drag-over {
|
.task-card.drag-over {
|
||||||
border: 2px dashed #667eea;
|
border-color: #4299e1;
|
||||||
background: #f8f9ff;
|
background: #1e3a5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-header {
|
.task-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-title {
|
.task-title {
|
||||||
font-size: 1.3em;
|
font-size: 1.1em;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #e4e6eb;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-size-badge {
|
.task-size-badge {
|
||||||
padding: 5px 12px;
|
padding: 4px 10px;
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
font-size: 0.8em;
|
font-size: 0.75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-size-badge.small {
|
.task-size-badge.small {
|
||||||
background: #e3f2fd;
|
background: #2c5282;
|
||||||
color: #1976d2;
|
color: #90cdf4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-size-badge.medium {
|
.task-size-badge.medium {
|
||||||
background: #fff3e0;
|
background: #7c2d12;
|
||||||
color: #f57c00;
|
color: #fed7aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-size-badge.large {
|
.task-size-badge.large {
|
||||||
background: #fce4ec;
|
background: #7c2d12;
|
||||||
color: #c2185b;
|
color: #fbb6ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-description {
|
.task-description {
|
||||||
color: #666;
|
color: #a0aec0;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Progress Bar */
|
/* Progress Bar */
|
||||||
.progress-section {
|
.progress-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-label {
|
.progress-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
font-size: 0.9em;
|
font-size: 0.85em;
|
||||||
color: #666;
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 25px;
|
height: 20px;
|
||||||
background: #f0f0f0;
|
background: #0f1419;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
background: #4299e1;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.85em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reward Section */
|
/* Reward Section */
|
||||||
.reward-section {
|
.reward-section {
|
||||||
background: #f8f9fa;
|
background: #2d3748;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 15px;
|
margin-top: 12px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reward-hidden {
|
.reward-hidden {
|
||||||
color: #999;
|
color: #718096;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reward-revealed {
|
.reward-revealed {
|
||||||
color: #28a745;
|
color: #68d391;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
animation: reveal 0.5s ease;
|
font-size: 1em;
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes reveal {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task Actions */
|
/* Task Actions */
|
||||||
.task-actions {
|
.task-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
margin-top: 12px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtasks */
|
/* Subtasks */
|
||||||
.subtasks-container {
|
.subtasks-container {
|
||||||
margin-top: 15px;
|
margin-top: 12px;
|
||||||
padding-left: 20px;
|
padding-left: 16px;
|
||||||
border-left: 3px solid #e0e0e0;
|
border-left: 2px solid #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask {
|
.subtask {
|
||||||
background: #f8f9fa;
|
background: #0f1419;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
cursor: move;
|
cursor: grab;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: 1px solid #2d3748;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask:hover {
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask.dragging {
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask-header {
|
.subtask-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask-title {
|
.subtask-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #e4e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask-progress {
|
.subtask-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
background: #e0e0e0;
|
background: #1a2332;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask-progress-fill {
|
.subtask-progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #4caf50 0%, #81c784 100%);
|
background: #48bb78;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtask-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Drop Zone */
|
/* Drop Zone */
|
||||||
.drop-zone {
|
.drop-zone {
|
||||||
border: 2px dashed #ccc;
|
border: 2px dashed #4a5568;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 15px;
|
padding: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: #718096;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
transition: all 0.3s;
|
font-size: 0.85em;
|
||||||
|
transition: all 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.visible {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 100px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone.drag-over {
|
.drop-zone.drag-over {
|
||||||
border-color: #667eea;
|
border-color: #4299e1;
|
||||||
background: #f0f4ff;
|
background: #1e3a5f;
|
||||||
color: #667eea;
|
color: #4299e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Completed State */
|
/* Completed State */
|
||||||
.completed {
|
.completed {
|
||||||
opacity: 0.7;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed .task-title {
|
.completed .task-title {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox Group */
|
/* Checkbox */
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group input[type="checkbox"] {
|
.checkbox-group input[type="checkbox"] {
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,34 +562,30 @@ button:active {
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: white;
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state h2 {
|
.empty-state h2 {
|
||||||
font-size: 2em;
|
font-size: 1.5em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
}
|
color: #cbd5e0;
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
font-size: 1.2em;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading State */
|
/* Loading State */
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: white;
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
border: 4px solid rgba(255,255,255,0.3);
|
border: 3px solid #2d3748;
|
||||||
border-top: 4px solid white;
|
border-top: 3px solid #4299e1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
margin: 0 auto 20px;
|
margin: 0 auto 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -468,24 +593,29 @@ button:active {
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
h1 {
|
||||||
width: 100%;
|
font-size: 1.5em;
|
||||||
right: -100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
header {
|
||||||
font-size: 2em;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-header {
|
.task-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-size-badge {
|
.task-size-badge {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-actions {
|
.task-actions {
|
||||||
|
|||||||
File renamed without changes.
Reference in new issue
Block a user