diff --git a/index.html b/index.html index 05f60ef..1b135eb 100644 --- a/index.html +++ b/index.html @@ -16,10 +16,6 @@ --orange: #ff9500; --yellow: #ffcc00; --border: #e5e5ea; - --sidebar-width: 220px; - --modal-width: 460px; - --palette-width: 480px; - --col-gap: 12px; } [data-theme="dark"] { @@ -51,7 +47,7 @@ body { .layout { display: grid; - grid-template-columns: var(--sidebar-width) 1fr; + grid-template-columns: 220px 1fr; min-height: 100vh; } @@ -89,7 +85,6 @@ body { text-align: left; font-size: 14px; color: var(--text); - transition: background 0.15s ease; } .sidebar button:hover { @@ -197,7 +192,7 @@ h2 { .board { display: grid; grid-template-columns: repeat(4, 1fr); - gap: var(--col-gap); + gap: 12px; } .column { @@ -205,25 +200,12 @@ h2 { border-radius: 8px; padding: 12px; min-height: 350px; - transition: box-shadow 0.2s ease; } .column.drag-over { box-shadow: 0 0 0 2px var(--accent); } -.column.drag-over::after { - content: 'Drop'; - display: block; - text-align: center; - padding: 10px; - color: var(--accent); - font-weight: 600; - border: 2px dashed var(--accent); - border-radius: 6px; - margin-top: 6px; -} - .column-head { display: flex; justify-content: space-between; @@ -251,7 +233,6 @@ h2 { padding: 10px; margin-bottom: 6px; cursor: grab; - transition: border-color 0.15s ease, transform 0.15s ease; } .task:hover { @@ -265,7 +246,6 @@ h2 { .task.dragging { opacity: 0.5; - transform: scale(0.98); } .task h3 { @@ -324,7 +304,7 @@ h2 { background: var(--surface); border-radius: 10px; padding: 24px; - width: var(--modal-width); + width: 460px; max-width: 90vw; max-height: 80vh; overflow-y: auto; @@ -388,7 +368,7 @@ h2 { .palette { background: var(--surface); border-radius: 10px; - width: var(--palette-width); + width: 480px; max-width: 90vw; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); } @@ -470,22 +450,23 @@ h2 { margin-bottom: 16px; } -.chart-wrap { - width: 100%; - height: 200px; -} - -.chart-wrap canvas { - width: 100%; - height: 100%; -} - .chart-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; } +.chart-canvas-wrap { + width: 100%; + height: 200px; + position: relative; +} + +.chart-canvas-wrap canvas { + width: 100%; + height: 100%; +} + .chart-legend { display: flex; flex-wrap: wrap; @@ -502,8 +483,8 @@ h2 { } .chart-legend .color-dot { - width: 8px; - height: 8px; + width: 10px; + height: 10px; border-radius: 50%; display: inline-block; } @@ -523,6 +504,17 @@ h2 { margin-bottom: 10px; } +.streak-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.streak-current, .streak-longest { + font-size: 12px; + font-weight: 600; +} + .streak-grid { display: grid; grid-template-columns: repeat(26, 1fr); @@ -651,14 +643,6 @@ h2 { .toast { animation: none !important; } - - .task { - transition: none !important; - } - - .sidebar button, .theme-btns button { - transition: none !important; - } } @@ -675,7 +659,7 @@ h2 {
- +
@@ -708,11 +692,11 @@ h2 {
Trend (Wochen)
-
+
Prioritätsverteilung
-
+
@@ -794,14 +778,12 @@ h2 { // === HILFSFUNKTIONEN === - /** Erzeugt eine eindeutige ID */ function generateUniqueId() { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).slice(2, 6); return 'd' + timestamp + '_' + random; } - /** Escape HTML-Entitäten für sichereres Renderen */ function htmlEscape(text) { if (!text) return ''; return text @@ -811,7 +793,6 @@ h2 { .replace(/"/g, '"'); } - /** Formatiert ein ISO-Datum als DD.MM.YYYY */ function formatDateString(isoDateString) { if (!isoDateString) return ''; try { @@ -821,7 +802,6 @@ h2 { } } - /** Zeigt einen temporären Toast-Nachricht an */ function showNotification(message, type) { const container = document.getElementById('toasts'); if (!container) return; @@ -839,7 +819,6 @@ h2 { }, 2600); } - /** Aktualisiert die Live-Region für Screenreader */ function announceStatus(message) { const liveRegion = document.getElementById('live-region'); if (liveRegion) { @@ -849,7 +828,6 @@ h2 { // === PERSISTENZ === - /** Speichert Tasks, Habits und Theme in localStorage */ function saveState() { try { localStorage.setItem('nv_t', JSON.stringify(tasks)); @@ -860,7 +838,6 @@ h2 { } } - /** Lädt Tasks, Habits und Theme aus localStorage (robust mit Validierung) */ function loadState() { try { const rawTasks = localStorage.getItem('nv_t'); @@ -890,14 +867,13 @@ h2 { registerPalette(); } - // === THEME / DARK MODE === + // === THEME === - /** Wendet das aktuell gespeicherte Theme auf die Seite an */ function applyTheme() { const mediaQuery = window.matchMedia('(prefers-color-scheme:dark)'); const documentElement = document.documentElement; - if (currentTheme === 'Systemeinstellung') { + if (currentTheme === 'system') { documentElement.setAttribute('data-theme', mediaQuery.matches ? 'dark' : 'light'); } else { documentElement.setAttribute('data-theme', currentTheme); @@ -910,19 +886,17 @@ h2 { } } - /** Hört auf Änderungen der Systemeinstellung und reagiert live */ function subscribeToSystemThemeChange() { const mediaQuery = window.matchMedia('(prefers-color-scheme:dark)'); mediaQuery.addEventListener('change', function () { - if (currentTheme === 'Systemeinstellung') { + if (currentTheme === 'system') { applyTheme(); } }); } - // === NAVIGATION / VIEWS === + // === NAVIGATION === - /** Schaltet die aktive View um und aktualisiert Hash für Persistenz */ function switchView(viewName) { currentView = viewName; location.hash = '#/' + viewName; @@ -941,7 +915,6 @@ h2 { renderView(); } - /** Rendert die aktuelle View dynamisch */ function renderView() { if (currentView === 'board') { renderBoard(); @@ -952,18 +925,15 @@ h2 { } } - /** Navigiert zum Board */ function clickOnTab(boardName) { switchView(boardName); } - // === EVENT-DELEGATION (Board) === + // === EVENT HOKS === - /** Weist die Event-Listener NUR EINMAL dem Board-Container zu */ function bindBoardEvents() { const boardSection = document.getElementById('board-section'); - // Klick-Delegation auf ".task" (Edit-Modal öffnen) boardSection.addEventListener('click', function (event) { const clickedTask = event.target.closest('.task'); if (!clickedTask) return; @@ -975,7 +945,6 @@ h2 { } }); - // Keyboard-Delegation für Löschen und Drag & Drop boardSection.addEventListener('keydown', function (event) { const focusedTask = document.querySelector('.task:focus, .task.selected'); if (!focusedTask) return; @@ -984,7 +953,6 @@ h2 { const task = tasks.find(function (t) { return t.id === taskId; }); if (!task) return; - // Delete-Taste: Task löschen if (event.key === 'Delete') { event.preventDefault(); const confirmed = confirm('Task wirklich löschen?'); @@ -999,14 +967,12 @@ h2 { return; } - // Tastatur-DnD: Ctrl+Alt + Pfeil if (event.ctrlKey && event.altKey) { event.preventDefault(); - moveTaskByKeyboard(taskId, event.key, task.st); + moveTaskByKeyboard(taskId, event.key, task.status); } }); - // Drag & Drop-Delegation boardSection.addEventListener('dragstart', onTaskDragStart); boardSection.addEventListener('dragover', onBoardDragOver); boardSection.addEventListener('dragenter', onBoardDragEnter); @@ -1014,19 +980,14 @@ h2 { boardSection.addEventListener('drop', onBoardDrop); } - /** Löscht die alten Event-Listener und weist sie NUR EINMAL erneut zu */ - function initBoardEventsOnce() { - if (initBoardEventsOnce.initialized) return; - initBoardEventsOnce.initialized = true; + let boardEventsBound = false; + if (!boardEventsBound) { bindBoardEvents(); + boardEventsBound = true; } - initBoardEventsOnce(); - // === BOARD RENDERING === - /** Rendert das Board: Header + Toolbar + Spalten. - * Toolbar bleibt im HTML stabl, wird NICHT neu gebaut. */ function renderBoard() { const query = document.getElementById('query').value.toLowerCase(); const priorityFilter = document.getElementById('priority-filter').value; @@ -1066,11 +1027,8 @@ h2 { document.getElementById('board').innerHTML = boardContent; } - // ========================= - // TASK MODAL (op()) - // ========================= + // === TASK MODAL === - /** Öffnet das Modal für ein neues oder bearbeitendes Task. */ function openTaskModal(task) { previousFocus = document.activeElement; isEditing = !!task; @@ -1079,16 +1037,15 @@ h2 { const description = task ? task.description : ''; const priority = task ? task.priority : 'Mittel'; const status = task ? task.status : 'aktiv'; - const sinceField = task && task.dueDate ? task.dueDate.slice(0, 10) : ''; + const dueField = task && task.dueDate ? task.dueDate.slice(0, 10) : ''; document.getElementById('modal-title').textContent = isEditing ? 'Task bearbeiten' : 'Neues Task'; document.getElementById('f-title').value = title; document.getElementById('f-desc').value = description; document.getElementById('f-priority').value = priority; - document.getElementById('f-due').value = sinceField; + document.getElementById('f-due').value = dueField; document.getElementById('f-delete').style.display = isEditing ? 'inline-flex' : 'none'; - // Status-Optionen aufbereitet let optionsHtml = ''; for (const column of COLUMNS) { optionsHtml += '' + htmlEscape(column[0]) + ''; @@ -1098,17 +1055,14 @@ h2 { document.getElementById('modal-bg').classList.add('open'); document.getElementById('f-title').focus(); - // Einmaligen Event-Handler setzen (keine Leaks!) bindModalHandlers(task); } - /** Weist die Modal-Handler EINMAL zu – verhindert doppelte Listener */ function bindModalHandlers(task) { const saveButton = document.getElementById('f-save'); const cancelButton = document.getElementById('f-cancel'); const deleteButton = document.getElementById('f-delete'); - // Alte Handler entfernen saveButton.replaceWith(saveButton.cloneNode(true)); cancelButton.replaceWith(cancelButton.cloneNode(true)); deleteButton.replaceWith(deleteButton.cloneNode(true)); @@ -1176,19 +1130,15 @@ h2 { }); } - /** Schließt das Task-Modal */ function closeTaskModal() { document.getElementById('modal-bg').classList.remove('open'); if (previousFocus) { - try { previousFocus.focus(); } catch (e) { /* Ignorieren */ } + try { previousFocus.focus(); } catch (e) { } } } - // ========================= - // STREAKS - // ========================= + // === STREAKS === - /** Rendert die Streak-Ansicht mit aktuellen + längsten Serie */ function renderStreaks() { const gridContainer = document.getElementById('streak-grid'); @@ -1202,8 +1152,11 @@ h2 { const currentStreak = calculateCurrentStreak(habit); const longestStreak = calculateLongestStreak(habit); - html += '

' + htmlEscape(habit.name) + - ' — Aktuelle Serie: ' + currentStreak + ' | Längste Serie: ' + longestStreak + '

'; + html += '

' + + htmlEscape(habit.name) + + '

Aktuell: ' + currentStreak + ' | ' + + 'Längste: ' + longestStreak + '
' + + '
'; for (let i = 0; i < 26; i++) { const day = new Date(); @@ -1219,8 +1172,7 @@ h2 { gridContainer.innerHTML = html; - // Einmaligen Click-Handler auf das Grid (Delegation) - gridContainer.onclick = function (event) { + gridContainer.addEventListener('click', function (event) { const cell = event.target.closest('.streak-cell'); if (!cell) return; @@ -1229,8 +1181,7 @@ h2 { cell.setAttribute('data-value', nextValue); const habitName = cell.closest('.streak').querySelector('h3').textContent; - const habitNameOnly = habitName.replace(' — Aktuelle Serie: .*', ''); - const habit = habits.find(function (h) { return h.name === habitNameOnly; }); + const habit = habits.find(function (h) { return h.name === habitName; }); if (habit && !habit.values) habit.values = {}; if (habit) { @@ -1238,10 +1189,9 @@ h2 { saveState(); renderStreaks(); } - }; + }); } - /** Berechnet die aktuelle Serie eines Habits (laufend seit dem letzten 0-Wert) */ function calculateCurrentStreak(habit) { if (!habit.values) return 0; let streak = 0; @@ -1254,11 +1204,9 @@ h2 { if (val === undefined || val === 0) break; streak++; } - // Heute noch nicht bewertet = kein Bruch, aber auch kein Zähl-Erwartung return streak; } - /** Berechnet die längste durchgehende Serie */ function calculateLongestStreak(habit) { if (!habit.values) return 0; let longest = 0; @@ -1272,18 +1220,15 @@ h2 { if (val > 0) { current++; longest = Math.max(longest, current); - } else if (val === 0 || val === undefined) { + } else { current = 0; } } return longest; } - // ========================= - // INSIGHTS (2 Charts) - // ========================= + // === INSIGHTS (2 Charts) === - /** Rendert Insights: Stats + Trend-Woche + Donut */ function renderInsights() { const totalTasks = tasks.length; let completed = 0; @@ -1306,7 +1251,6 @@ h2 { drawDonutChart(); } - /** Zieht Daten in Wochen-Intervallen aus finishedAt (df-Feld) */ function drawTrendChart() { const canvas = document.getElementById('trend-chart'); if (!canvas) return; @@ -1319,7 +1263,6 @@ h2 { const now = new Date(); const weekData = []; - // Letzte 12 Wochen for (let w = 0; w < 12; w++) { const weekStart = new Date(now); weekStart.setDate(weekStart.getDate() - (w * 7) - weekStart.getDay() + 1); @@ -1351,7 +1294,6 @@ h2 { const barWidth = 30; const gap = (graphWidth / weekData.length - barWidth) * 0.5; - // Gitter & Y-Achsen-Label ctx.strokeStyle = 'rgba(0,0,0,0.15)'; ctx.lineWidth = 1; for (let i = 0; i <= 4; i++) { @@ -1367,7 +1309,6 @@ h2 { ctx.fillText(Math.round(maxCount / 4 * i), marginLeft - 5, y + 4); } - // Balken ctx.fillStyle = '#0071e3'; for (let i = 0; i < weekData.length; i++) { const x = marginLeft + i * (barWidth / 1.5 + gap / 2); @@ -1376,7 +1317,6 @@ h2 { ctx.fillRect(x, y, barWidth, height); - // Label ctx.fillStyle = '#6e6e73'; ctx.textAlign = 'center'; ctx.font = '9px sans-serif'; @@ -1389,7 +1329,6 @@ h2 { } } - /** Zeichnet den Donut-Chart (Prioritätsverteilung) */ function drawDonutChart() { const canvas = document.getElementById('donut-chart'); if (!canvas) return; @@ -1428,13 +1367,11 @@ h2 { startAngle += slice; } - // Zentrum ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(centerX, centerY, innerRadius, 0, Math.PI * 2); ctx.fill(); - // Legende const legendEl = document.getElementById('donut-legend'); if (legendEl) { legendEl.innerHTML = ''; @@ -1446,11 +1383,8 @@ h2 { } } - // ========================= - // COMMAND PALETTE - // ========================= + // === COMMAND PALETTE === - /** Registriert die Befehle in der Palette (≥8 Einträge, inkl. Tasks als anspringbar) */ function registerPalette() { const commands = [ { name: '📅 Board', shortcut: 'Cmd+K→B', action: function () { switchView('board'); } }, @@ -1465,7 +1399,6 @@ h2 { { name: '⚡ 500 Tasks generieren', shortcut: 'Cmd+K→5', action: function () { generate500Tasks(); } }, ]; - // Dynamisch Tasks als anspringbare Einträge ergänzen (max. 5) const recentTasks = tasks.slice(-5).reverse(); for (const task of recentTasks) { commands.push({ @@ -1478,29 +1411,25 @@ h2 { paletteCommand = commands; } - /** Baut die Palette-Liste aus den registrierten Befehlen auf */ function buildPaletteList() { paletteIndex = 0; let html = ''; for (let i = 0; i < paletteCommand.length; i++) { - html += '
' + htmlEscape(paletteCommand[i].name) + + html += '
' + + '' + htmlEscape(paletteCommand[i].name) + '' + htmlEscape(paletteCommand[i].shortcut) + '
'; } document.getElementById('pal-list').innerHTML = html; - // Einmalige Event-Handler const items = document.querySelectorAll('#pal-list .palette-item'); for (const item of items) { - // Hover-Indizierung item.addEventListener('mouseenter', function () { paletteIndex = parseInt(this.getAttribute('data-index'), 10); highlightPaletteIndex(); }); - // Klick führt Befehl aus item.addEventListener('click', function () { const command = paletteCommand[parseInt(this.getAttribute('data-index'), 10)]; if (command) command.action(); @@ -1508,7 +1437,6 @@ h2 { } } - /** Markiert den aktuell selektierten Palette-Eintrag visuell */ function highlightPaletteIndex() { const items = document.querySelectorAll('#pal-list .palette-item'); for (let i = 0; i < items.length; i++) { @@ -1516,7 +1444,6 @@ h2 { } } - /** Filtert Palette-Einträge nach Suchbegriff */ function filterPaletteItems() { const query = document.getElementById('pal-q').value.toLowerCase(); const items = document.querySelectorAll('#pal-list .palette-item'); @@ -1527,7 +1454,6 @@ h2 { } } - /** Öffnet die Command-Palette */ function openPalette() { document.getElementById('pal-bg').classList.add('open'); document.getElementById('pal-q').value = ''; @@ -1536,35 +1462,29 @@ h2 { buildPaletteList(); } - /** Schließt die Command-Palette */ function closePalette() { document.getElementById('pal-bg').classList.remove('open'); } - // Klick auf Hintergrund schließt Palette document.querySelector('.palette-background').addEventListener('click', function (event) { if (event.target.id === 'pal-bg') { closePalette(); } }); - // Globale Tasten-Kurzverbindungen document.addEventListener('keydown', function (event) { - // Cmd+K oder Strg+K → Palette öffnen if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); openPalette(); return; } - // Escape → Modal + Palette schließen if (event.key === 'Escape') { closeTaskModal(); setTimeout(closePalette, 50); } }); - // Palette-Input Listener (Suche + Pfeiltasten + Enter + Tab-Trap) document.getElementById('pal-q').addEventListener('input', function () { buildPaletteList(); filterPaletteItems(); @@ -1576,20 +1496,30 @@ h2 { if (event.key === 'ArrowDown') { event.preventDefault(); - const maxIndex = Math.min(paletteIndex + 1, visibleItems.length - 1); - // Finde die tatsächliche Position im vollständigen Array - let visibleCount = 0; - for (let i = 0; i < paletteIndex; i++) { - if (!paletteCommand[i].name) continue; - const domEl = document.querySelector('#pal-list .palette-item[data-index="' + i + '"]'); - if (domEl && domEl.style.display !== 'none') visibleCount++; + const allItems = document.querySelectorAll('#pal-list .palette-item'); + const indices = []; + for (const item of allItems) { + if (item.style.display !== 'none') { + indices.push(parseInt(item.getAttribute('data-index'), 10)); + } } - paletteIndex = findNextVisibleIndex(paletteIndex, 1, visibleItems); - if (paletteIndex !== -1) highlightPaletteIndex(); + const pos = indices.indexOf(paletteIndex); + const next = pos >= indices.length - 1 ? indices[0] : indices[pos + 1]; + paletteIndex = next; + highlightPaletteIndex(); } else if (event.key === 'ArrowUp') { event.preventDefault(); - paletteIndex = findNextVisibleIndex(paletteIndex, -1, visibleItems); - if (paletteIndex !== -1) highlightPaletteIndex(); + const allItems = document.querySelectorAll('#pal-list .palette-item'); + const indices = []; + for (const item of allItems) { + if (item.style.display !== 'none') { + indices.push(parseInt(item.getAttribute('data-index'), 10)); + } + } + const pos = indices.indexOf(paletteIndex); + const prev = pos <= 0 ? indices[indices.length - 1] : indices[pos - 1]; + paletteIndex = prev; + highlightPaletteIndex(); } else if (event.key === 'Enter') { const selected = document.querySelector('#pal-list .palette-item.selected'); if (selected) selected.click(); @@ -1600,34 +1530,8 @@ h2 { } }); - /** Hilft bei der Navigation durch sichtbare Palette-Einträge */ - function findNextVisibleIndex(currentIndex, direction, visibleItems) { - const allItems = document.querySelectorAll('#pal-list .palette-item'); - const visibleMap = new Map(); - for (const item of allItems) { - if (item.style.display !== 'none') { - visibleMap.set(parseInt(item.getAttribute('data-index'), 10), true); - } - } + // === DRAG & DROP === - const indices = Array.from(allItems).map(function (el) { return parseInt(el.getAttribute('data-index'), 10); }).filter(function (i) { - return visibleMap.has(i); - }); - - const pos = indices.indexOf(currentIndex); - if (pos === -1) return direction > 0 ? indices[0] : indices[indices.length - 1]; - - const next = pos + direction; - if (next < 0) return indices[indices.length - 1]; - if (next >= indices.length) return indices[0]; - return indices[next]; - } - - // ========================= - // DRAG & DROP - // ========================= - - /** Wird ausgelöst, wenn ein Task zum Draggen anfängt */ function onTaskDragStart(event) { const task = event.target.closest('.task'); if (!task) return; @@ -1641,27 +1545,23 @@ h2 { event.target.addEventListener('dragend', onTaskDragEnd); } - /** Entfernt das dragging-Flag vom Task beim Ende eines DnD */ function onTaskDragEnd(event) { const task = event.target; task.classList.remove('dragging'); task.removeEventListener('dragend', onTaskDragEnd); } - /** Erlaubt das Drop auf den Board-Container */ function onBoardDragOver(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; } - /** Hebt die Ziel-Spalte visuell hervor */ function onBoardDragEnter(event) { event.preventDefault(); const column = event.target.closest('.column'); if (column) column.classList.add('drag-over'); } - /** Entfernt die Highlighting der Ziel-Spalte */ function onBoardDragLeave(event) { const column = event.target.closest('.column'); if (column && !column.contains(event.relatedTarget)) { @@ -1669,12 +1569,10 @@ h2 { } } - /** Verarbeitet den Drop: Status ändern oder Reihenfolge anpassen */ function onBoardDrop(event) { event.preventDefault(); dragSourceId = event.dataTransfer.getData('text/plain'); - // Alles Highlighting aufräumen document.querySelectorAll('.column').forEach(function (col) { col.classList.remove('drag-over'); }); @@ -1684,15 +1582,13 @@ h2 { const task = tasks.find(function (t) { return t.id === dragSourceId; }); if (!task) return; - // Ziel-Spalte bestimmen const targetColumn = event.target.closest('.column'); if (!targetColumn) return; const targetStatus = targetColumn.getAttribute('data-status'); if (targetStatus === task.status) { - // Gleiche Spalte = Reihenfolge anpassen const targetTask = event.target.closest('.task'); - if (targetTask && !!targetTask.getAttribute('data-id')) { + if (targetTask && targetTask.getAttribute('data-id')) { const targetId = targetTask.getAttribute('data-id'); if (targetId === task.id) return; @@ -1704,14 +1600,12 @@ h2 { } } } else { - // Neue Spalte = Status ändern const fromIndex = tasks.indexOf(task); if (fromIndex !== -1) tasks.splice(fromIndex, 1); task.status = targetStatus; if (targetStatus === 'fertig') task.finishedAt = Date.now(); - // Am Ende der Spalte einfügen tasks.push(task); } @@ -1720,18 +1614,14 @@ h2 { showNotification('Gezogen zu ' + targetStatus, 'grn'); } - // ========================= - // TASTATUR-DRAG & DROP - // ========================= + // === TASTATUR-DAG & DROP === - /** Bewegt ein fokussiertes Task per Tastatur (Ctrl+Alt+Pfeil) */ function moveTaskByKeyboard(taskId, arrowKey, currentStatus) { const columnIndex = COLUMNS.findIndex(function (c) { return c[1] === currentStatus; }); const task = tasks.find(function (t) { return t.id === taskId; }); if (!task) return; if (arrowKey === 'ArrowLeft' || arrowKey === 'ArrowRight') { - // Status-Spalte wechseln const newStatusIndex = (arrowKey === 'ArrowRight') ? Math.min(columnIndex + 1, COLUMNS.length - 1) : Math.max(columnIndex - 1, 0); @@ -1740,14 +1630,12 @@ h2 { if (task.status === 'fertig') task.finishedAt = Date.now(); else task.finishedAt = null; - // Ans Ende der Spalte schieben const oldIndex = tasks.indexOf(task); if (oldIndex !== -1) tasks.splice(oldIndex, 1); tasks.push(task); showNotification('Spalte: ' + COLUMNS[newStatusIndex][0], 'grn'); } else if (arrowKey === 'ArrowUp' || arrowKey === 'ArrowDown') { - // Position in aktueller Spalte wechseln const spaltenTasks = tasks.filter(function (t) { return t.status === currentStatus; }); const currentIndex = spaltenTasks.findIndex(function (t) { return t.id === taskId; }); @@ -1757,14 +1645,12 @@ h2 { const targetId = spaltenTasks[newIndex].id; - // Task aus Array auslagern, neu an position einfügen const taskOldIndex = tasks.indexOf(task); if (taskOldIndex !== -1) tasks.splice(taskOldIndex, 1); const newTaskIndex = tasks.findIndex(function (t) { return t.id === targetId; }); tasks.splice(newTaskIndex, 0, task); - // Fokus auf neues Position-Element setzen const newFocused = document.querySelector('.task[data-id="' + taskId + '"]'); if (newFocused) newFocused.focus(); } @@ -1774,11 +1660,8 @@ h2 { announceStatus('Task verschoben'); } - // ========================= - // EXPORT / IMPORT - // ========================= + // === EXPORT / IMPORT === - /** Exportiert Tasks, Habits und Theme als herabladbares JSON-Blob */ function doExport() { const exportData = { version: 1, @@ -1802,7 +1685,6 @@ h2 { showNotification('Export OK', 'grn'); } - /** Importiert Daten aus JSON-Datei mit Validierung */ function doImport(file) { if (!file) return; @@ -1811,18 +1693,16 @@ h2 { try { const imported = JSON.parse(event.target.result); - // Validierung: version + tasks Array if (!imported || typeof imported !== 'object') { throw new Error('keine JSON-Struktur'); } if (imported.version !== 1 && imported.version !== undefined) { - // Nicht fatal: alte Dateien verarbeiten + // Alte Version, akzeptiert } if (!imported.tasks || !Array.isArray(imported.tasks)) { throw new Error('tasks fehlt oder ist kein Array'); } - // Jeden Task validieren const validTasks = []; const invalidCount = []; for (const task of imported.tasks) { @@ -1830,7 +1710,6 @@ h2 { invalidCount.push(task.title || task.id); continue; } - // Erweitere um bestehende Felder wenn vorhanden validTasks.push({ id: task.id, title: task.title, @@ -1854,7 +1733,7 @@ h2 { } if (imported.theme) { currentTheme = imported.theme; - applyTheme(); + applyTheme(); } saveState(); @@ -1863,7 +1742,7 @@ h2 { showNotification('Import OK (' + validTasks.length + ' Tasks, ' + invalidCount.length + ' ignoriert)', 'grn'); if (invalidCount.length > 0) { - alert('Einige Tasks wurden nicht importiert (fehlende id/titel/Status): ' + invalidCount.join(', ')); + alert('Einige Tasks wurden nicht importiert (fehlende id/Title/Status): ' + invalidCount.join(', ')); } } catch (error) { @@ -1873,20 +1752,16 @@ h2 { reader.readAsText(file); } - // File-Input für Import-Dialog document.getElementById('imp').addEventListener('change', function (event) { const file = event.target.files[0]; if (file) doImport(file); - event.target.value = ''; // Reset + event.target.value = ''; }); - // ========================= - // DEMO-DATEN - // ========================= + // === DEMO-DATEN === let demoLoaded = false; - /** Füllt die App mit Demo-Daten (wenn leer) */ function loadDemoData() { if (demoLoaded && tasks.length > 0) return; demoLoaded = true; @@ -1914,17 +1789,18 @@ h2 { }); } - habits = [ + const habitDefaults = [ { name: 'Code', values: generateHabitValues(15) }, { name: 'Sport', values: generateHabitValues(10) }, { name: 'Lesen', values: generateHabitValues(8) }, { name: 'Meditation', values: generateHabitValues(12) }, ]; + habits.length = 0; + habits.push(...habitDefaults); saveState(); } - /** Generiert zufällige Habit-Werte für n Tage */ function generateHabitValues(days) { const values = {}; for (let i = 0; i < days; i++) { @@ -1935,11 +1811,8 @@ h2 { return values; } - // ========================= - // 500 TASKS GENERIEREN (Performance: Batch) - // ========================= + // === 500 TASKS GENERIEREN (Performance: Batch) === - /** Generiert 500 Tasks in einem Batch (kein Einzel-Push pro Task) */ function generate500Tasks() { const batch = []; for (let i = 0; i < 500; i++) { @@ -1959,22 +1832,17 @@ h2 { }); } - // Batch-Push statt Einzel-Push für Performance tasks.splice(0, 0, ...batch); saveState(); renderView(); showNotification('500 Tasks generiert', 'grn'); } - // ========================= - // HASH-ROUTING - // ========================= + // === HASH-ROUTING === - /** Lauscht auf URL-Hash-Änderungen und schaltet die View */ function setupHashRouting() { const views = ['board', 'streaks', 'insights']; - // Beim Seitenaufruf: View aus Hash lesen function handleHashChange() { const hash = location.hash.replace('#/', ''); if (views.includes(hash)) { @@ -1992,16 +1860,12 @@ h2 { } window.addEventListener('hashchange', handleHashChange); - - // Initiale Hash-Analyse handleHashChange(); } setupHashRouting(); - // ========================= - // SIDEBAR-CLICK EVENTS - // ========================= + // === SIDEBAR-CLICK EVENTS === document.querySelectorAll('.sidebar button[data-view]').forEach(function (button) { button.addEventListener('click', function () { @@ -2010,7 +1874,6 @@ h2 { }); }); - // Theme Buttons document.querySelectorAll('.theme-btns button').forEach(function (button) { button.addEventListener('click', function () { currentTheme = button.getAttribute('data-theme-value'); @@ -2020,38 +1883,30 @@ h2 { }); }); - // Neues Task Button (einmalig) document.getElementById('btn-new').addEventListener('click', function () { openTaskModal(null); }); - // Toolbar-Events (einmalig, keine Leaks) const queryInput = document.getElementById('query'); const prioritySelect = document.getElementById('priority-filter'); - let renderTimeout = null; + let queryRenderTimeout = null; queryInput.addEventListener('input', function () { - clearTimeout(renderTimeout); - renderTimeout = setTimeout(renderBoard, 150); + clearTimeout(queryRenderTimeout); + queryRenderTimeout = setTimeout(renderBoard, 150); }); prioritySelect.addEventListener('change', renderBoard); - // Suche: display:none statt DOM-Neuaufbau — bereits integriert (Query-Filter in renderBoard) - - // ========================= - // APP START - // ========================= + // === APP START === loadState(); applyTheme(); subscribeToSystemThemeChange(); - // Demo-Daten bei leerem LocalStorage if (tasks.length === 0 && habits.length === 0) { loadDemoData(); } - // View initial rendern renderView(); })();