Edited completed Task + Delte HTML Windows
This commit is contained in:
1 parent
e5e0703106
commit
def7d22294
3 files changed
+504
-122
No files matched your search
+374
-84
@@ -50,6 +50,7 @@ class App {
|
|||||||
async init() {
|
async init() {
|
||||||
this.setupGreeting();
|
this.setupGreeting();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
this.setupContainerDrag();
|
||||||
await this.loadTasks();
|
await this.loadTasks();
|
||||||
await this.loadArchivedCount();
|
await this.loadArchivedCount();
|
||||||
}
|
}
|
||||||
@@ -257,6 +258,30 @@ class App {
|
|||||||
|
|
||||||
hideContextMenu() { document.getElementById('context-menu').classList.remove('show'); }
|
hideContextMenu() { document.getElementById('context-menu').classList.remove('show'); }
|
||||||
|
|
||||||
|
// ─── Confirm Dialog ───
|
||||||
|
|
||||||
|
showConfirm(message, icon = '🗑') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const overlay = document.getElementById('confirm-overlay');
|
||||||
|
document.getElementById('confirm-message').innerHTML = message;
|
||||||
|
document.getElementById('confirm-icon').innerHTML = icon;
|
||||||
|
overlay.classList.add('open');
|
||||||
|
|
||||||
|
const cancel = document.getElementById('confirm-cancel');
|
||||||
|
const del = document.getElementById('confirm-delete');
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
overlay.classList.remove('open');
|
||||||
|
cancel.replaceWith(cancel.cloneNode(true));
|
||||||
|
del.replaceWith(del.cloneNode(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('confirm-cancel').addEventListener('click', () => { cleanup(); resolve(false); });
|
||||||
|
document.getElementById('confirm-delete').addEventListener('click', () => { cleanup(); resolve(true); });
|
||||||
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); resolve(false); } }, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleContextEdit() {
|
handleContextEdit() {
|
||||||
if (this.contextMenuTarget) this.openEditModal(this.contextMenuTarget, this.contextMenuParent);
|
if (this.contextMenuTarget) this.openEditModal(this.contextMenuTarget, this.contextMenuParent);
|
||||||
this.hideContextMenu();
|
this.hideContextMenu();
|
||||||
@@ -264,10 +289,14 @@ class App {
|
|||||||
|
|
||||||
async handleContextDelete() {
|
async handleContextDelete() {
|
||||||
if (!this.contextMenuTarget) return;
|
if (!this.contextMenuTarget) return;
|
||||||
if (!confirm(`Delete this ${this.contextMenuType}?`)) { this.hideContextMenu(); return; }
|
const type = this.contextMenuType;
|
||||||
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();
|
this.hideContextMenu();
|
||||||
|
const confirmed = await this.showConfirm(
|
||||||
|
`Delete this ${type}?<br><span style="font-size:0.85em;color:var(--text-muted)">This action cannot be undone.</span>`
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
if (type === 'task') this.deleteTask(this.contextMenuTarget.id);
|
||||||
|
else if (type === 'subtask' && this.contextMenuParent) this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleContextPromote() {
|
async handleContextPromote() {
|
||||||
@@ -497,6 +526,13 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async confirmDeleteTask(taskId) {
|
||||||
|
const confirmed = await this.showConfirm(
|
||||||
|
'Delete permanently?<br><span style="font-size:0.85em;color:var(--text-muted)">This action cannot be undone.</span>'
|
||||||
|
);
|
||||||
|
if (confirmed) this.deleteTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
toggleTaskComplete(taskId) {
|
toggleTaskComplete(taskId) {
|
||||||
const task = this.getCurrentTasks().find(t => t.id === taskId);
|
const task = this.getCurrentTasks().find(t => t.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
@@ -589,26 +625,169 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Drag & Drop ───
|
// ─── Drag & Drop (insertion indicator system) ───
|
||||||
|
|
||||||
|
_createDropIndicator() {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'drop-indicator';
|
||||||
|
el.style.display = 'none';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showDropIndicator(container, y) {
|
||||||
|
if (!this._dropIndicator) this._dropIndicator = this._createDropIndicator();
|
||||||
|
const cards = [...container.querySelectorAll(':scope > .task-card')];
|
||||||
|
let insertIndex = cards.length;
|
||||||
|
let indicatorY = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < cards.length; i++) {
|
||||||
|
const rect = cards[i].getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
if (y < midY) {
|
||||||
|
insertIndex = i;
|
||||||
|
indicatorY = rect.top - 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If after all cards
|
||||||
|
if (insertIndex === cards.length && cards.length > 0) {
|
||||||
|
const lastRect = cards[cards.length - 1].getBoundingClientRect();
|
||||||
|
indicatorY = lastRect.bottom + 4;
|
||||||
|
} else if (cards.length === 0) {
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
indicatorY = containerRect.top + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
this._dropIndicator.style.display = 'block';
|
||||||
|
this._dropIndicator.style.top = indicatorY + 'px';
|
||||||
|
this._dropIndicator.style.left = containerRect.left + 'px';
|
||||||
|
this._dropIndicator.style.width = containerRect.width + 'px';
|
||||||
|
this._dropInsertIndex = insertIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideDropIndicator() {
|
||||||
|
if (this._dropIndicator) this._dropIndicator.style.display = 'none';
|
||||||
|
this._dropInsertIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSubtaskDropIndicator(subtaskContainer, y) {
|
||||||
|
if (!this._subtaskDropIndicator) {
|
||||||
|
this._subtaskDropIndicator = document.createElement('div');
|
||||||
|
this._subtaskDropIndicator.className = 'drop-indicator drop-indicator-subtask';
|
||||||
|
this._subtaskDropIndicator.style.display = 'none';
|
||||||
|
document.body.appendChild(this._subtaskDropIndicator);
|
||||||
|
}
|
||||||
|
const subtasks = [...subtaskContainer.querySelectorAll(':scope > .subtask')];
|
||||||
|
let insertIndex = subtasks.length;
|
||||||
|
let indicatorY = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < subtasks.length; i++) {
|
||||||
|
const rect = subtasks[i].getBoundingClientRect();
|
||||||
|
if (y < rect.top + rect.height / 2) {
|
||||||
|
insertIndex = i;
|
||||||
|
indicatorY = rect.top - 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex === subtasks.length && subtasks.length > 0) {
|
||||||
|
const lastRect = subtasks[subtasks.length - 1].getBoundingClientRect();
|
||||||
|
indicatorY = lastRect.bottom + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cRect = subtaskContainer.getBoundingClientRect();
|
||||||
|
this._subtaskDropIndicator.style.display = 'block';
|
||||||
|
this._subtaskDropIndicator.style.top = indicatorY + 'px';
|
||||||
|
this._subtaskDropIndicator.style.left = cRect.left + 'px';
|
||||||
|
this._subtaskDropIndicator.style.width = cRect.width + 'px';
|
||||||
|
this._subtaskDropInsertIndex = insertIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideSubtaskDropIndicator() {
|
||||||
|
if (this._subtaskDropIndicator) this._subtaskDropIndicator.style.display = 'none';
|
||||||
|
this._subtaskDropInsertIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupContainerDrag() {
|
||||||
|
const container = document.getElementById('tasks-container');
|
||||||
|
|
||||||
|
container.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.draggedTask) return;
|
||||||
|
this._showDropIndicator(container, e.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('dragleave', (e) => {
|
||||||
|
// Only hide if leaving the container entirely
|
||||||
|
if (!container.contains(e.relatedTarget)) {
|
||||||
|
this._hideDropIndicator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Handle nesting via drop zone
|
||||||
|
const dropZone = e.target.closest('.drop-zone');
|
||||||
|
if (dropZone) {
|
||||||
|
const taskCard = dropZone.closest('.task-card');
|
||||||
|
if (taskCard) {
|
||||||
|
const taskId = taskCard.getAttribute('data-task-id');
|
||||||
|
const targetTask = this.tasks.find(t => t.id === taskId);
|
||||||
|
if (targetTask) this._handleNestDrop(targetTask);
|
||||||
|
}
|
||||||
|
this._hideDropIndicator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reorder via indicator
|
||||||
|
if (this.draggedTask && this._dropInsertIndex != null) {
|
||||||
|
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
|
||||||
|
let targetIndex = this._dropInsertIndex;
|
||||||
|
|
||||||
|
// Adjust index since we're removing before inserting
|
||||||
|
if (currentIndex !== -1 && currentIndex < targetIndex) targetIndex--;
|
||||||
|
if (currentIndex !== -1 && currentIndex !== targetIndex) {
|
||||||
|
this.tasks.splice(currentIndex, 1);
|
||||||
|
this.tasks.splice(targetIndex, 0, this.draggedTask);
|
||||||
|
this._syncReorder();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._hideDropIndicator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleDragStart(e, item, isSubtask = false, parentTask = null, index = null) {
|
handleDragStart(e, item, isSubtask = false, parentTask = null, index = null) {
|
||||||
if (isSubtask) { this.draggedSubtask = item; this.draggedParent = parentTask; this.draggedSubtaskIndex = index; this.draggedTask = null; }
|
if (isSubtask) {
|
||||||
else { this.draggedTask = item; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null; }
|
this.draggedSubtask = item; this.draggedParent = parentTask;
|
||||||
|
this.draggedSubtaskIndex = index; this.draggedTask = null;
|
||||||
|
} else {
|
||||||
|
this.draggedTask = item; this.draggedSubtask = null;
|
||||||
|
this.draggedParent = null; this.draggedSubtaskIndex = null;
|
||||||
|
}
|
||||||
e.currentTarget.classList.add('dragging');
|
e.currentTarget.classList.add('dragging');
|
||||||
|
// Delay to let browser capture drag image
|
||||||
|
requestAnimationFrame(() => e.currentTarget.style.opacity = '0.4');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnd(e) {
|
handleDragEnd(e) {
|
||||||
e.currentTarget.classList.remove('dragging');
|
e.currentTarget.classList.remove('dragging');
|
||||||
|
e.currentTarget.style.opacity = '';
|
||||||
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||||
document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('visible'));
|
document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('visible'));
|
||||||
|
this._hideDropIndicator();
|
||||||
|
this._hideSubtaskDropIndicator();
|
||||||
if (this.dropZoneTimer) { clearTimeout(this.dropZoneTimer); this.dropZoneTimer = null; }
|
if (this.dropZoneTimer) { clearTimeout(this.dropZoneTimer); this.dropZoneTimer = null; }
|
||||||
this.draggedTask = null; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null;
|
this.draggedTask = null; this.draggedSubtask = null;
|
||||||
|
this.draggedParent = null; this.draggedSubtaskIndex = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCardDragOver(e) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('drag-over'); }
|
handleDragEnterCard(e, taskId) {
|
||||||
handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); }
|
|
||||||
|
|
||||||
handleDragEnter(e, taskId) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.draggedTask && !this.draggedSubtask) return;
|
if (!this.draggedTask && !this.draggedSubtask) return;
|
||||||
if (this.dropZoneTimer) clearTimeout(this.dropZoneTimer);
|
if (this.dropZoneTimer) clearTimeout(this.dropZoneTimer);
|
||||||
@@ -618,8 +797,7 @@ class App {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDrop(e, targetTask) {
|
_handleNestDrop(targetTask) {
|
||||||
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
|
|
||||||
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
|
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
|
||||||
targetTask.subtasks.push(this.draggedTask);
|
targetTask.subtasks.push(this.draggedTask);
|
||||||
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
|
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
|
||||||
@@ -637,58 +815,34 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleTaskReorder(e, targetIndex) {
|
handleSubtaskContainerDragOver(e, task) {
|
||||||
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
|
e.preventDefault();
|
||||||
if (!this.draggedTask) return;
|
e.stopPropagation();
|
||||||
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
|
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) return;
|
||||||
if (currentIndex === -1 || currentIndex === targetIndex) return;
|
const subtaskContainer = e.currentTarget;
|
||||||
|
this._showSubtaskDropIndicator(subtaskContainer, e.clientY);
|
||||||
// Update data
|
|
||||||
this.tasks.splice(currentIndex, 1);
|
|
||||||
this.tasks.splice(targetIndex, 0, this.draggedTask);
|
|
||||||
|
|
||||||
// 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) {
|
handleSubtaskContainerDrop(e, task) {
|
||||||
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
|
e.preventDefault();
|
||||||
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== parentTask.id) return;
|
e.stopPropagation();
|
||||||
if (this.draggedSubtaskIndex === targetIndex) return;
|
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) {
|
||||||
|
this._hideSubtaskDropIndicator();
|
||||||
// Update data
|
return;
|
||||||
const subtask = parentTask.subtasks[this.draggedSubtaskIndex];
|
|
||||||
parentTask.subtasks.splice(this.draggedSubtaskIndex, 1);
|
|
||||||
parentTask.subtasks.splice(targetIndex, 0, subtask);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (this._subtaskDropInsertIndex == null) { this._hideSubtaskDropIndicator(); return; }
|
||||||
|
|
||||||
this._syncToServer(parentTask);
|
let targetIndex = this._subtaskDropInsertIndex;
|
||||||
|
const currentIndex = this.draggedSubtaskIndex;
|
||||||
|
if (currentIndex < targetIndex) targetIndex--;
|
||||||
|
if (currentIndex !== targetIndex) {
|
||||||
|
const subtask = task.subtasks[currentIndex];
|
||||||
|
task.subtasks.splice(currentIndex, 1);
|
||||||
|
task.subtasks.splice(targetIndex, 0, subtask);
|
||||||
|
this._syncToServer(task);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
this._hideSubtaskDropIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Render: Active ───
|
// ─── Render: Active ───
|
||||||
@@ -702,15 +856,9 @@ class App {
|
|||||||
card.setAttribute('data-task-id', task.id);
|
card.setAttribute('data-task-id', task.id);
|
||||||
card.style.borderLeftColor = task.color || '#6366f1';
|
card.style.borderLeftColor = task.color || '#6366f1';
|
||||||
|
|
||||||
card.addEventListener('dragstart', (e) => this.handleDragStart(e, task, false));
|
card.addEventListener('dragstart', (e) => { if (!e.target.closest('.subtask')) this.handleDragStart(e, task, false); });
|
||||||
card.addEventListener('dragend', (e) => this.handleDragEnd(e));
|
card.addEventListener('dragend', (e) => this.handleDragEnd(e));
|
||||||
card.addEventListener('dragover', (e) => this.handleCardDragOver(e));
|
card.addEventListener('dragenter', (e) => this.handleDragEnterCard(e, task.id));
|
||||||
card.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
|
||||||
card.addEventListener('dragenter', (e) => this.handleDragEnter(e, task.id));
|
|
||||||
card.addEventListener('drop', (e) => {
|
|
||||||
if (e.target.closest('.drop-zone')) this.handleDrop(e, task);
|
|
||||||
else if (this.draggedTask && this.draggedTask.id !== task.id) this.handleTaskReorder(e, index);
|
|
||||||
});
|
|
||||||
card.addEventListener('contextmenu', (e) => { if (!e.target.closest('.subtask')) this.showContextMenu(e, task, 'task'); });
|
card.addEventListener('contextmenu', (e) => { if (!e.target.closest('.subtask')) this.showContextMenu(e, task, 'task'); });
|
||||||
card.addEventListener('dblclick', (e) => { if (!e.target.closest('.subtask') && !e.target.closest('button')) this.openEditModal(task); });
|
card.addEventListener('dblclick', (e) => { if (!e.target.closest('.subtask') && !e.target.closest('button')) this.openEditModal(task); });
|
||||||
|
|
||||||
@@ -775,6 +923,13 @@ class App {
|
|||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'subtasks-container';
|
container.className = 'subtasks-container';
|
||||||
|
|
||||||
|
// Container-level drag events for subtask reorder
|
||||||
|
container.addEventListener('dragover', (e) => this.handleSubtaskContainerDragOver(e, task));
|
||||||
|
container.addEventListener('drop', (e) => this.handleSubtaskContainerDrop(e, task));
|
||||||
|
container.addEventListener('dragleave', (e) => {
|
||||||
|
if (!container.contains(e.relatedTarget)) this._hideSubtaskDropIndicator();
|
||||||
|
});
|
||||||
|
|
||||||
task.subtasks.forEach((subtask, index) => {
|
task.subtasks.forEach((subtask, index) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'subtask';
|
div.className = 'subtask';
|
||||||
@@ -783,12 +938,6 @@ class App {
|
|||||||
|
|
||||||
div.addEventListener('dragstart', (e) => { e.stopPropagation(); this.handleDragStart(e, subtask, true, task, index); });
|
div.addEventListener('dragstart', (e) => { e.stopPropagation(); this.handleDragStart(e, subtask, true, task, index); });
|
||||||
div.addEventListener('dragend', (e) => { e.stopPropagation(); this.handleDragEnd(e); });
|
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); });
|
div.addEventListener('contextmenu', (e) => { e.stopPropagation(); this.showContextMenu(e, subtask, 'subtask', task); });
|
||||||
div.addEventListener('dblclick', (e) => { e.stopPropagation(); if (!e.target.closest('button')) this.openEditModal(subtask, task); });
|
div.addEventListener('dblclick', (e) => { e.stopPropagation(); if (!e.target.closest('button')) this.openEditModal(subtask, task); });
|
||||||
|
|
||||||
@@ -878,7 +1027,7 @@ class App {
|
|||||||
` : ''}
|
` : ''}
|
||||||
<div class="task-actions">
|
<div class="task-actions">
|
||||||
<button class="btn-small btn-restore" onclick="app.unarchiveTask('${task.id}')">Restore</button>
|
<button class="btn-small btn-restore" onclick="app.unarchiveTask('${task.id}')">Restore</button>
|
||||||
<button class="btn-small btn-delete" onclick="if(confirm('Delete permanently?')) app.deleteTask('${task.id}')">Delete</button>
|
<button class="btn-small btn-delete" onclick="app.confirmDeleteTask('${task.id}')">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -954,14 +1103,155 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fireBigCelebration() {
|
fireBigCelebration() {
|
||||||
this.fireConfetti(0.5, 0.4, 80);
|
const canvas = document.createElement('canvas');
|
||||||
setTimeout(() => this.fireConfetti(0.25, 0.5, 50), 300);
|
canvas.className = 'confetti-canvas';
|
||||||
setTimeout(() => this.fireConfetti(0.75, 0.5, 50), 600);
|
canvas.width = window.innerWidth;
|
||||||
const overlay = document.createElement('div');
|
canvas.height = window.innerHeight;
|
||||||
overlay.className = 'celebration-overlay';
|
document.body.appendChild(canvas);
|
||||||
overlay.innerHTML = '<div class="celebration-text">All Goals Complete!</div>';
|
const ctx = canvas.getContext('2d');
|
||||||
document.body.appendChild(overlay);
|
|
||||||
setTimeout(() => overlay.remove(), 3200);
|
const palettes = [
|
||||||
|
['#6366f1', '#818cf8', '#a5b4fc'],
|
||||||
|
['#f43f5e', '#fb7185', '#fda4af'],
|
||||||
|
['#f59e0b', '#fbbf24', '#fde68a'],
|
||||||
|
['#10b981', '#34d399', '#6ee7b7'],
|
||||||
|
['#0ea5e9', '#38bdf8', '#7dd3fc'],
|
||||||
|
['#ec4899', '#f472b6', '#f9a8d4'],
|
||||||
|
['#8b5cf6', '#a78bfa', '#c4b5fd'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const particles = [];
|
||||||
|
const rockets = [];
|
||||||
|
let startTime = performance.now();
|
||||||
|
const duration = 4500;
|
||||||
|
const launchTimes = [0, 300, 550, 900, 1200, 1600, 2000, 2400, 2800, 3200];
|
||||||
|
|
||||||
|
const launchRocket = () => {
|
||||||
|
const x = 0.15 + Math.random() * 0.7;
|
||||||
|
const targetY = 0.15 + Math.random() * 0.35;
|
||||||
|
rockets.push({
|
||||||
|
x: x * canvas.width, y: canvas.height,
|
||||||
|
targetY: targetY * canvas.height,
|
||||||
|
speed: 6 + Math.random() * 4,
|
||||||
|
palette: palettes[Math.floor(Math.random() * palettes.length)],
|
||||||
|
trail: [], alive: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const explode = (x, y, palette) => {
|
||||||
|
const count = 50 + Math.floor(Math.random() * 40);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = 1.5 + Math.random() * 5;
|
||||||
|
particles.push({
|
||||||
|
x, y,
|
||||||
|
vx: Math.cos(angle) * speed,
|
||||||
|
vy: Math.sin(angle) * speed,
|
||||||
|
color: palette[Math.floor(Math.random() * palette.length)],
|
||||||
|
size: 1.5 + Math.random() * 2.5,
|
||||||
|
life: 1,
|
||||||
|
decay: 0.012 + Math.random() * 0.01,
|
||||||
|
trail: [],
|
||||||
|
sparkle: Math.random() > 0.6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let launchIndex = 0;
|
||||||
|
let frame;
|
||||||
|
|
||||||
|
const animate = (now) => {
|
||||||
|
const elapsed = now - startTime;
|
||||||
|
|
||||||
|
while (launchIndex < launchTimes.length && elapsed >= launchTimes[launchIndex]) {
|
||||||
|
launchRocket();
|
||||||
|
launchIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear to transparent each frame
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw rockets
|
||||||
|
for (const r of rockets) {
|
||||||
|
if (!r.alive) continue;
|
||||||
|
r.trail.push({ x: r.x, y: r.y });
|
||||||
|
if (r.trail.length > 8) r.trail.shift();
|
||||||
|
r.y -= r.speed;
|
||||||
|
|
||||||
|
for (let i = 0; i < r.trail.length; i++) {
|
||||||
|
const t = r.trail[i];
|
||||||
|
const alpha = (i / r.trail.length) * 0.7;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillStyle = r.palette[0];
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(t.x, t.y, 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.shadowColor = r.palette[0];
|
||||||
|
ctx.shadowBlur = 8;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(r.x, r.y, 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
if (r.y <= r.targetY) {
|
||||||
|
r.alive = false;
|
||||||
|
explode(r.x, r.y, r.palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw particles with trails
|
||||||
|
for (const p of particles) {
|
||||||
|
if (p.life <= 0) continue;
|
||||||
|
p.trail.push({ x: p.x, y: p.y, life: p.life });
|
||||||
|
if (p.trail.length > 5) p.trail.shift();
|
||||||
|
p.x += p.vx; p.y += p.vy;
|
||||||
|
p.vy += 0.04;
|
||||||
|
p.vx *= 0.985; p.vy *= 0.985;
|
||||||
|
p.life -= p.decay;
|
||||||
|
|
||||||
|
// Trail
|
||||||
|
for (let i = 0; i < p.trail.length; i++) {
|
||||||
|
const t = p.trail[i];
|
||||||
|
ctx.globalAlpha = (i / p.trail.length) * t.life * 0.35;
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(t.x, t.y, p.size * 0.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main particle with glow
|
||||||
|
ctx.globalAlpha = p.life;
|
||||||
|
if (p.sparkle && Math.random() > 0.5) {
|
||||||
|
ctx.globalAlpha = p.life * (0.5 + Math.random() * 0.5);
|
||||||
|
}
|
||||||
|
ctx.shadowColor = p.color;
|
||||||
|
ctx.shadowBlur = 6;
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
|
const allDone = elapsed > duration &&
|
||||||
|
rockets.every(r => !r.alive) &&
|
||||||
|
particles.every(p => p.life <= 0);
|
||||||
|
|
||||||
|
if (allDone) {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
canvas.remove();
|
||||||
|
} else {
|
||||||
|
frame = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
frame = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
fireConfetti(originX = 0.5, originY = 0.5, count = 50) {
|
fireConfetti(originX = 0.5, originY = 0.5, count = 50) {
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
<div class="context-menu-item danger" id="context-delete">🗑 Delete</div>
|
<div class="context-menu-item danger" id="context-delete">🗑 Delete</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Dialog -->
|
||||||
|
<div class="confirm-overlay" id="confirm-overlay">
|
||||||
|
<div class="confirm-dialog">
|
||||||
|
<div class="confirm-icon" id="confirm-icon">🗑</div>
|
||||||
|
<div class="confirm-message" id="confirm-message">Are you sure?</div>
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<button class="btn-confirm-cancel" id="confirm-cancel">Cancel</button>
|
||||||
|
<button class="btn-confirm-delete" id="confirm-delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="modal" id="modal">
|
<div class="modal" id="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
+118
-38
@@ -188,6 +188,94 @@ h1 {
|
|||||||
.context-menu-item:hover { background: var(--accent-soft); color: var(--accent); }
|
.context-menu-item:hover { background: var(--accent-soft); color: var(--accent); }
|
||||||
.context-menu-item.danger:hover { background: var(--rose-soft); color: var(--rose); }
|
.context-menu-item.danger:hover { background: var(--rose-soft); color: var(--rose); }
|
||||||
|
|
||||||
|
/* ─── Confirm Dialog ─── */
|
||||||
|
.confirm-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
z-index: 2000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-overlay.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 28px 32px 22px;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 360px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
animation: confirmIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes confirmIn {
|
||||||
|
from { opacity: 0; transform: scale(0.92) translateY(8px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon {
|
||||||
|
font-size: 2.2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-message {
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-cancel,
|
||||||
|
.btn-confirm-delete {
|
||||||
|
padding: 8px 22px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.88em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-cancel {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-cancel:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-delete {
|
||||||
|
background: var(--rose);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(225, 29, 72, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-delete:hover {
|
||||||
|
background: #be123c;
|
||||||
|
box-shadow: 0 4px 12px rgba(225, 29, 72, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Modal ─── */
|
/* ─── Modal ─── */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -502,11 +590,6 @@ button {
|
|||||||
.task-card:active { cursor: grabbing; }
|
.task-card:active { cursor: grabbing; }
|
||||||
.task-card.dragging { opacity: 0.4; }
|
.task-card.dragging { opacity: 0.4; }
|
||||||
|
|
||||||
.task-card.drag-over {
|
|
||||||
border-color: var(--accent);
|
|
||||||
background: rgba(99, 102, 241, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card.archived-card {
|
.task-card.archived-card {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@@ -873,48 +956,46 @@ button {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.celebration-overlay {
|
/* ─── Drop Indicator ─── */
|
||||||
|
.drop-indicator {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0; left: 0;
|
height: 3px;
|
||||||
width: 100%; height: 100%;
|
background: var(--accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 100;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 9998;
|
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
|
||||||
display: flex;
|
transition: top 0.08s ease;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
animation: celebFade 3s ease forwards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.celebration-text {
|
.drop-indicator::before,
|
||||||
font-size: 3em;
|
.drop-indicator::after {
|
||||||
font-weight: 800;
|
content: '';
|
||||||
background: linear-gradient(135deg, #6366f1, #a78bfa, #f59e0b, #10b981);
|
position: absolute;
|
||||||
background-size: 300% 300%;
|
top: -4px;
|
||||||
-webkit-background-clip: text;
|
width: 11px;
|
||||||
-webkit-text-fill-color: transparent;
|
height: 11px;
|
||||||
background-clip: text;
|
background: var(--accent);
|
||||||
animation: celebPulse 2s ease, celebGradient 2s ease infinite;
|
border-radius: 50%;
|
||||||
filter: drop-shadow(0 2px 8px rgba(99, 102, 241, 0.3));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes celebFade {
|
.drop-indicator::before { left: -5px; }
|
||||||
0% { opacity: 0; }
|
.drop-indicator::after { right: -5px; }
|
||||||
15% { opacity: 1; }
|
|
||||||
70% { opacity: 1; }
|
.drop-indicator-subtask {
|
||||||
100% { opacity: 0; }
|
height: 2px;
|
||||||
|
box-shadow: 0 0 6px rgba(99, 102, 241, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes celebPulse {
|
.drop-indicator-subtask::before,
|
||||||
0% { transform: scale(0.3); }
|
.drop-indicator-subtask::after {
|
||||||
50% { transform: scale(1.1); }
|
width: 8px;
|
||||||
100% { transform: scale(1); }
|
height: 8px;
|
||||||
|
top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes celebGradient {
|
.drop-indicator-subtask::before { left: -4px; }
|
||||||
0% { background-position: 0% 50%; }
|
.drop-indicator-subtask::after { right: -4px; }
|
||||||
50% { background-position: 100% 50%; }
|
|
||||||
100% { background-position: 0% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card.just-completed {
|
.task-card.just-completed {
|
||||||
animation: completeFlash 0.6s ease;
|
animation: completeFlash 0.6s ease;
|
||||||
@@ -939,5 +1020,4 @@ button {
|
|||||||
.form-row-2 { grid-template-columns: 1fr; }
|
.form-row-2 { grid-template-columns: 1fr; }
|
||||||
.task-card { padding: 10px 12px; }
|
.task-card { padding: 10px 12px; }
|
||||||
.greeting-banner .greeting-text { font-size: 1em; }
|
.greeting-banner .greeting-text { font-size: 1em; }
|
||||||
.celebration-text { font-size: 2em; }
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user