';
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();
})();