Drag on Drop Tausch verbessert

This commit is contained in:
Honey committed 2026-02-18 01:34:12 +01:00
1 parent 01035f0834
commit e5e0703106
2 files changed
+153 -57

No files matched your search

+149 -52
View File
@@ -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
View File
@@ -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; }