Drag on Drop Tausch verbessert
This commit is contained in:
1 parent
01035f0834
commit
e5e0703106
2 files changed
+153
-57
No files matched your search
+149
-52
@@ -276,11 +276,24 @@ class App {
|
||||
if (!parentTask) { this.hideContextMenu(); return; }
|
||||
const subtask = this.contextMenuTarget;
|
||||
parentTask.subtasks = parentTask.subtasks.filter(st => st.id !== subtask.id);
|
||||
|
||||
// Create new task locally
|
||||
const newTask = new Task(Date.now().toString(), subtask.title, subtask.description, subtask.size, subtask.reward);
|
||||
newTask.dueDate = subtask.dueDate;
|
||||
newTask.color = subtask.color;
|
||||
newTask.completed = subtask.completed;
|
||||
this.tasks.push(newTask);
|
||||
|
||||
this._syncToServer(parentTask);
|
||||
try {
|
||||
await fetch(`${API_URL}/tasks/${parentTask.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(parentTask) });
|
||||
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, dueDate: subtask.dueDate, color: subtask.color }) });
|
||||
await this.loadTasks();
|
||||
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, dueDate: subtask.dueDate, color: subtask.color })
|
||||
});
|
||||
} catch (error) { console.error('Error promoting subtask:', error); }
|
||||
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
this.hideContextMenu();
|
||||
}
|
||||
|
||||
@@ -318,23 +331,32 @@ class App {
|
||||
const subtask = parentTask.subtasks.find(st => st.id === editId);
|
||||
if (subtask) {
|
||||
Object.assign(subtask, { title, description, size, reward, expandDescription, dueDate, color });
|
||||
await this.updateTask(parentTask);
|
||||
this._syncToServer(parentTask);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const task = taskList.find(t => t.id === editId);
|
||||
if (task) {
|
||||
Object.assign(task, { title, description, size, reward, expandDescription, dueDate, color });
|
||||
await this.updateTask(task);
|
||||
this._syncToServer(task);
|
||||
}
|
||||
}
|
||||
this.updateOverallProgress();
|
||||
if (this.viewMode === 'archive') this.renderArchive();
|
||||
else this.render();
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, description, size, reward, expandDescription, dueDate, color })
|
||||
});
|
||||
if (response.ok) await this.loadTasks();
|
||||
if (response.ok) {
|
||||
const newTaskData = await response.json();
|
||||
const newTask = this._mapTaskData(newTaskData);
|
||||
this.tasks.push(newTask);
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
}
|
||||
} catch (error) { console.error('Error creating task:', error); }
|
||||
}
|
||||
|
||||
@@ -369,14 +391,12 @@ class App {
|
||||
|
||||
async loadTasks() {
|
||||
try {
|
||||
const prevProgress = this._previousOverallProgress;
|
||||
const response = await fetch(`${API_URL}/tasks`);
|
||||
const data = await response.json();
|
||||
this.tasks = data.map(d => this._mapTaskData(d));
|
||||
this.updateOverallProgress();
|
||||
if (this.viewMode === 'active') this.render();
|
||||
this.loadArchivedCount();
|
||||
if (this._initialized) this._checkCelebrations(prevProgress);
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Error loading tasks:', error);
|
||||
@@ -441,72 +461,111 @@ class App {
|
||||
document.getElementById('overall-progress-fill').style.width = overallProgress + '%';
|
||||
}
|
||||
|
||||
// ─── Task Actions ───
|
||||
// ─── Server Sync (fire-and-forget) ───
|
||||
|
||||
async deleteTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${taskId}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
if (this.viewMode === 'archive') await this.loadArchivedTasks();
|
||||
else await this.loadTasks();
|
||||
}
|
||||
} catch (error) { console.error('Error deleting task:', error); }
|
||||
_syncToServer(task) {
|
||||
fetch(`${API_URL}/tasks/${task.id}`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task)
|
||||
}).catch(err => console.error('Sync error:', err));
|
||||
}
|
||||
|
||||
async toggleTaskComplete(taskId) {
|
||||
_syncDelete(taskId) {
|
||||
fetch(`${API_URL}/tasks/${taskId}`, { method: 'DELETE' })
|
||||
.catch(err => console.error('Sync error:', err));
|
||||
}
|
||||
|
||||
_syncReorder() {
|
||||
fetch(`${API_URL}/tasks/reorder`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.tasks.map(t => t.id))
|
||||
}).catch(err => console.error('Sync error:', err));
|
||||
}
|
||||
|
||||
// ─── Task Actions (optimistic) ───
|
||||
|
||||
deleteTask(taskId) {
|
||||
if (this.viewMode === 'archive') {
|
||||
this.archivedTasks = this.archivedTasks.filter(t => t.id !== taskId);
|
||||
this._syncDelete(taskId);
|
||||
this.renderArchive();
|
||||
this.updateArchiveCount();
|
||||
} else {
|
||||
this.tasks = this.tasks.filter(t => t.id !== taskId);
|
||||
this._syncDelete(taskId);
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
toggleTaskComplete(taskId) {
|
||||
const task = this.getCurrentTasks().find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
const prevProgress = this._previousOverallProgress;
|
||||
const wasCompleted = task.completed || task.calculateProgress() === 100;
|
||||
task.completed = !task.completed;
|
||||
if (task.subtasks.length > 0) task.subtasks.forEach(st => st.completed = task.completed);
|
||||
const isNowCompleted = task.completed || task.calculateProgress() === 100;
|
||||
if (!wasCompleted && isNowCompleted && task.reward) this._celebrateTaskId = taskId;
|
||||
await this.updateTask(task);
|
||||
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
this._checkCelebrations(prevProgress);
|
||||
this._syncToServer(task);
|
||||
}
|
||||
|
||||
async toggleSubtaskComplete(taskId, subtaskId) {
|
||||
toggleSubtaskComplete(taskId, subtaskId) {
|
||||
const task = this.getCurrentTasks().find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
const subtask = task.subtasks.find(st => st.id === subtaskId);
|
||||
if (!subtask) return;
|
||||
const prevProgress = this._previousOverallProgress;
|
||||
const wasSubCompleted = subtask.completed;
|
||||
subtask.completed = !subtask.completed;
|
||||
if (!wasSubCompleted && subtask.completed && subtask.reward) this._celebrateTaskId = taskId;
|
||||
const parentWillComplete = task.calculateProgress() === 100;
|
||||
if (parentWillComplete && task.reward && !task.completed) this._celebrateTaskId = taskId;
|
||||
await this.updateTask(task);
|
||||
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
this._checkCelebrations(prevProgress);
|
||||
this._syncToServer(task);
|
||||
}
|
||||
|
||||
async deleteSubtask(taskId, subtaskId) {
|
||||
deleteSubtask(taskId, subtaskId) {
|
||||
const task = this.getCurrentTasks().find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
task.subtasks = task.subtasks.filter(st => st.id !== subtaskId);
|
||||
await this.updateTask(task);
|
||||
}
|
||||
|
||||
async updateTask(task) {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${task.id}`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task)
|
||||
});
|
||||
if (response.ok) {
|
||||
if (this.viewMode === 'archive') await this.loadArchivedTasks();
|
||||
else await this.loadTasks();
|
||||
}
|
||||
} catch (error) { console.error('Error updating task:', error); }
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
this._syncToServer(task);
|
||||
}
|
||||
|
||||
async archiveTask(taskId) {
|
||||
// Move from active to archived locally
|
||||
const taskIndex = this.tasks.findIndex(t => t.id === taskId);
|
||||
if (taskIndex === -1) return;
|
||||
const [task] = this.tasks.splice(taskIndex, 1);
|
||||
this.archivedTasks.push(task);
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
this.updateArchiveCount();
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${taskId}/archive`, { method: 'PUT' });
|
||||
if (response.ok) { await this.loadTasks(); await this.loadArchivedCount(); }
|
||||
await fetch(`${API_URL}/tasks/${taskId}/archive`, { method: 'PUT' });
|
||||
} catch (error) { console.error('Error archiving task:', error); }
|
||||
}
|
||||
|
||||
async unarchiveTask(taskId) {
|
||||
// Move from archived back to active
|
||||
const taskIndex = this.archivedTasks.findIndex(t => t.id === taskId);
|
||||
if (taskIndex === -1) return;
|
||||
const [task] = this.archivedTasks.splice(taskIndex, 1);
|
||||
this.tasks.push(task);
|
||||
this.renderArchive();
|
||||
this.updateArchiveCount();
|
||||
// Also update active progress in background
|
||||
this.updateOverallProgress();
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${taskId}/unarchive`, { method: 'PUT' });
|
||||
if (response.ok) { await this.loadArchivedTasks(); await this.loadTasks(); }
|
||||
await fetch(`${API_URL}/tasks/${taskId}/unarchive`, { method: 'PUT' });
|
||||
} catch (error) { console.error('Error restoring task:', error); }
|
||||
}
|
||||
|
||||
@@ -563,13 +622,18 @@ class App {
|
||||
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
|
||||
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
|
||||
targetTask.subtasks.push(this.draggedTask);
|
||||
try { await fetch(`${API_URL}/tasks/${this.draggedTask.id}`, { method: 'DELETE' }); await this.updateTask(targetTask); }
|
||||
catch (error) { console.error('Error moving task:', error); }
|
||||
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
|
||||
this._syncDelete(this.draggedTask.id);
|
||||
this._syncToServer(targetTask);
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
} else if (this.draggedSubtask && this.draggedParent && targetTask.id !== this.draggedParent.id) {
|
||||
this.draggedParent.subtasks = this.draggedParent.subtasks.filter(st => st.id !== this.draggedSubtask.id);
|
||||
targetTask.subtasks.push(this.draggedSubtask);
|
||||
try { await this.updateTask(this.draggedParent); await this.updateTask(targetTask); }
|
||||
catch (error) { console.error('Error moving subtask:', error); }
|
||||
this._syncToServer(this.draggedParent);
|
||||
this._syncToServer(targetTask);
|
||||
this.updateOverallProgress();
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,22 +642,53 @@ class App {
|
||||
if (!this.draggedTask) return;
|
||||
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
|
||||
if (currentIndex === -1 || currentIndex === targetIndex) return;
|
||||
|
||||
// Update data
|
||||
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)) });
|
||||
this.render();
|
||||
} catch (error) { console.error('Error reordering:', error); await this.loadTasks(); }
|
||||
|
||||
// Swap DOM nodes directly
|
||||
const container = document.getElementById('tasks-container');
|
||||
const cards = [...container.children];
|
||||
const draggedCard = cards[currentIndex];
|
||||
if (draggedCard) {
|
||||
draggedCard.remove();
|
||||
const referenceCard = container.children[targetIndex];
|
||||
if (referenceCard) container.insertBefore(draggedCard, referenceCard);
|
||||
else container.appendChild(draggedCard);
|
||||
}
|
||||
|
||||
// Sync to server in background
|
||||
this._syncReorder();
|
||||
}
|
||||
|
||||
async handleSubtaskReorder(e, parentTask, targetIndex) {
|
||||
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
|
||||
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== parentTask.id) return;
|
||||
if (this.draggedSubtaskIndex === targetIndex) return;
|
||||
|
||||
// Update data
|
||||
const subtask = parentTask.subtasks[this.draggedSubtaskIndex];
|
||||
parentTask.subtasks.splice(this.draggedSubtaskIndex, 1);
|
||||
parentTask.subtasks.splice(targetIndex, 0, subtask);
|
||||
await this.updateTask(parentTask);
|
||||
|
||||
// Swap subtask DOM nodes
|
||||
const card = document.querySelector(`[data-task-id="${parentTask.id}"]`);
|
||||
if (card) {
|
||||
const subtaskContainer = card.querySelector('.subtasks-container');
|
||||
if (subtaskContainer) {
|
||||
const subtaskNodes = [...subtaskContainer.children];
|
||||
const draggedNode = subtaskNodes[this.draggedSubtaskIndex];
|
||||
if (draggedNode) {
|
||||
draggedNode.remove();
|
||||
const ref = subtaskContainer.children[targetIndex];
|
||||
if (ref) subtaskContainer.insertBefore(draggedNode, ref);
|
||||
else subtaskContainer.appendChild(draggedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._syncToServer(parentTask);
|
||||
}
|
||||
|
||||
// ─── Render: Active ───
|
||||
@@ -735,12 +830,13 @@ class App {
|
||||
|
||||
render() {
|
||||
const container = document.getElementById('tasks-container');
|
||||
container.innerHTML = '';
|
||||
if (this.tasks.length === 0) {
|
||||
container.innerHTML = `<div class="empty-state"><h2>No goals yet</h2><p>Click "+ New Goal" to get started</p></div>`;
|
||||
return;
|
||||
}
|
||||
this.tasks.forEach((task, index) => container.appendChild(this.renderTask(task, index)));
|
||||
const fragment = document.createDocumentFragment();
|
||||
this.tasks.forEach((task, index) => fragment.appendChild(this.renderTask(task, index)));
|
||||
container.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
// ─── Render: Archive ───
|
||||
@@ -830,12 +926,13 @@ class App {
|
||||
|
||||
renderArchive() {
|
||||
const container = document.getElementById('tasks-container');
|
||||
container.innerHTML = '';
|
||||
if (this.archivedTasks.length === 0) {
|
||||
container.innerHTML = `<div class="empty-state"><h2>Archive is empty</h2><p>Completed tasks you archive will appear here</p></div>`;
|
||||
return;
|
||||
}
|
||||
this.archivedTasks.forEach(task => container.appendChild(this.renderArchivedTask(task)));
|
||||
const fragment = document.createDocumentFragment();
|
||||
this.archivedTasks.forEach(task => fragment.appendChild(this.renderArchivedTask(task)));
|
||||
container.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
// ─── Celebrations ───
|
||||
|
||||
+4
-5
@@ -9,8 +9,8 @@
|
||||
--card-border: rgba(255, 255, 255, 0.45);
|
||||
--card-hover: rgba(255, 255, 255, 0.7);
|
||||
--card-shadow: 0 2px 12px rgba(60, 60, 120, 0.1);
|
||||
--subtask-bg: rgba(255, 255, 255, 0.25);
|
||||
--subtask-border: rgba(255, 255, 255, 0.2);
|
||||
--subtask-bg: rgba(255, 255, 255, 0.14);
|
||||
--subtask-border: rgba(255, 255, 255, 0.12);
|
||||
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #475569;
|
||||
@@ -491,7 +491,6 @@ button {
|
||||
border: 1px solid var(--card-border);
|
||||
border-left: 4px solid var(--accent);
|
||||
position: relative;
|
||||
animation: cardIn 0.25s ease;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
@@ -728,8 +727,8 @@ button {
|
||||
.subtask:last-child { margin-bottom: 0; }
|
||||
|
||||
.subtask:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-color: rgba(99, 102, 241, 0.25);
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-color: rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.subtask:active { cursor: grabbing; }
|
||||
|
||||
Reference in new issue
Block a user