Files
nova-control/index.html
T
Dann Kra 321140ff86 fix: Alle JS-Syntax-Fehler behoben
- showNotification import OK-Zeile vervollständigt
- loadDemoData() speichert korrekte im localStorage
- Template-String/Literal-Handling korrigiert
 Alle Gates geschlossen (18/18)
2026-06-28 22:51:36 +00:00

1915 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>NOVA Control</title>
<style>
:root {
--bg: #f5f5f7;
--surface: #ffffff;
--surface-hover: #f5f5f7;
--text: #1d1d1f;
--secondary: #6e6e73;
--accent: #0071e3;
--green: #34c759;
--red: #ff3b30;
--orange: #ff9500;
--yellow: #ffcc00;
--border: #e5e5ea;
}
[data-theme="dark"] {
--bg: #1c1c1e;
--surface: #2c2c2e;
--surface-hover: #3a3a3c;
--text: #f5f5f7;
--secondary: #a1a1a6;
--accent: #0a84ff;
--green: #30d158;
--red: #ff453a;
--orange: #ff9f0a;
--yellow: #ffd60a;
--border: #38383a;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
overflow: auto;
}
.layout {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 100vh;
}
/* SIDEBAR */
.sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
padding: 20px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
position: sticky;
top: 0;
height: 100vh;
}
.sidebar .logo {
padding: 10px 20px;
font-size: 22px;
font-weight: 800;
color: var(--accent);
border-bottom: 1px solid var(--border);
}
.sidebar nav {
flex-grow: 1;
}
.sidebar button {
width: 100%;
padding: 10px 20px;
border: none;
background: none;
cursor: pointer;
text-align: left;
font-size: 14px;
color: var(--text);
}
.sidebar button:hover {
background: var(--surface-hover);
}
.sidebar button.selected {
background: var(--accent);
color: #ffffff;
}
.theme-btns {
padding: 10px 20px;
display: flex;
gap: 6px;
border-top: 1px solid var(--border);
}
.theme-btns button {
flex: 1;
padding: 6px;
border: 1px solid var(--border);
background: var(--bg);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.theme-btns button.selected {
background: var(--accent);
color: #ffffff;
border-color: var(--accent);
}
/* CONTENT */
.content {
padding: 24px;
overflow-y: auto;
height: 100vh;
}
.section {
display: none;
}
.section.visible {
display: block;
}
h2 {
font-size: 22px;
font-weight: 700;
margin-bottom: 16px;
}
/* TOOLBAR */
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 16px;
align-items: center;
flex-wrap: wrap;
}
.toolbar input, .toolbar select {
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
background: var(--surface);
color: var(--text);
}
.toolbar input[type="text"] {
flex: 1;
min-width: 120px;
}
.toolbar select {
padding: 6px 10px;
}
/* BUTTONS */
.button {
padding: 6px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
.button.primary {
background: var(--accent);
color: #ffffff;
}
.button.secondary {
background: var(--surface-hover);
color: var(--text);
border: 1px solid var(--border);
}
/* BOARD */
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.column {
background: var(--surface);
border-radius: 8px;
padding: 12px;
min-height: 350px;
}
.column.drag-over {
box-shadow: 0 0 0 2px var(--accent);
}
.column-head {
display: flex;
justify-content: space-between;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary);
padding-bottom: 8px;
border-bottom: 2px solid var(--border);
margin-bottom: 8px;
}
.task-count {
background: var(--bg);
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
color: var(--secondary);
}
.task {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
margin-bottom: 6px;
cursor: grab;
}
.task:hover {
border-color: var(--accent);
}
.task:focus {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.task.dragging {
opacity: 0.5;
}
.task h3 {
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.task p {
font-size: 11px;
color: var(--secondary);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task .meta {
display: flex;
justify-content: space-between;
font-size: 10px;
}
.badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
}
.badge.high { background: #ffebee; color: #c62828; }
.badge.medium { background: #fff3e0; color: #e65100; }
.badge.low { background: #e8f5e9; color: #2e7d32; }
[data-theme="dark"] .badge.high { background: #5c1a16; color: #ff8a80; }
[data-theme="dark"] .badge.medium { background: #5c3910; color: #ffcc80; }
[data-theme="dark"] .badge.low { background: #1b4316; color: #81c784; }
/* MODAL */
.modal-background {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-background.open {
display: flex;
}
.modal {
background: var(--surface);
border-radius: 10px;
padding: 24px;
width: 460px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
}
.modal h3 {
font-size: 16px;
font-weight: 700;
margin-bottom: 16px;
}
.field {
margin-bottom: 12px;
}
.field label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--secondary);
margin-bottom: 3px;
}
.field input, .field textarea, .field select {
width: 100%;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: 5px;
font-size: 13px;
background: var(--surface);
color: var(--text);
}
.field textarea {
resize: vertical;
min-height: 60px;
}
.modal-actions {
display: flex;
justify-content: space-between;
margin-top: 16px;
}
/* PALETTE */
.palette-background {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: none;
align-items: flex-start;
justify-content: center;
padding-top: 10vh;
z-index: 200;
}
.palette-background.open {
display: flex;
}
.palette {
background: var(--surface);
border-radius: 10px;
width: 480px;
max-width: 90vw;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}
.palette input {
width: 100%;
padding: 14px 18px;
border: none;
font-size: 15px;
background: transparent;
outline: none;
color: var(--text);
}
.palette-list {
max-height: 50vh;
overflow-y: auto;
}
.palette-item {
padding: 10px 18px;
display: flex;
justify-content: space-between;
border-radius: 6px;
cursor: pointer;
}
.palette-item:hover, .palette-item.selected {
background: var(--surface-hover);
}
.palette-item .name {
font-size: 13px;
}
.palette-item .shortcut {
font-size: 10px;
color: var(--secondary);
background: var(--bg);
border: 1px solid var(--border);
padding: 2px 5px;
border-radius: 3px;
}
/* STATS */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
text-align: center;
}
.stat-card .value {
font-size: 24px;
font-weight: 800;
color: var(--accent);
}
.stat-card .label {
font-size: 11px;
color: var(--secondary);
margin-top: 4px;
}
/* CHARTS */
.chart {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.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;
gap: 12px;
margin-top: 8px;
font-size: 11px;
color: var(--secondary);
}
.chart-legend span {
display: flex;
align-items: center;
gap: 4px;
}
.chart-legend .color-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
/* STREAK */
.streak {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
margin-bottom: 14px;
}
.streak h3 {
font-size: 14px;
font-weight: 600;
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);
gap: 2px;
}
.streak-cell {
width: 12px;
height: 12px;
border-radius: 2px;
background: var(--border);
outline: none;
cursor: pointer;
}
.streak-cell[data-value="1"] { background: #a5d6a7; }
.streak-cell[data-value="2"] { background: #66bb6a; }
.streak-cell[data-value="3"] { background: #2e7d32; }
.streak-cell[data-value="4"] { background: #1b5e20; }
.streak-cell:hover {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.streak-cell:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* TOAST */
.toast-container {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 300;
display: flex;
flex-direction: column;
gap: 4px;
}
.toast {
padding: 8px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: #ffffff;
animation: toastIn 0.3s ease;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.toast.success { background: var(--green); }
.toast.error { background: var(--red); }
.toast.info { background: var(--accent); }
/* SCREEN READER ONLY */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: auto;
width: 100%;
flex-direction: row;
justify-content: space-between;
padding: 8px 16px;
z-index: 50;
}
.sidebar nav {
display: flex;
gap: 4px;
flex-grow: 0;
}
.sidebar button {
padding: 8px 12px;
font-size: 12px;
}
.theme-btns {
display: none;
}
.content {
height: calc(100vh - 56px);
padding-bottom: 72px;
}
}
@media (max-width: 480px) {
.board {
grid-template-columns: 1fr;
gap: 8px;
}
.column {
min-height: auto;
}
}
/* REDUCED MOTION */
@media (prefers-reduced-motion: reduce) {
.toast {
animation: none !important;
}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar" role="navigation" aria-label="Hauptnavigation">
<div>
<div class="logo">NOVA</div>
<nav>
<button class="selected" data-view="board" aria-label="Board anzeigen">📅 Board</button>
<button data-view="streaks" aria-label="Streaks anzeigen">🔥 Streaks</button>
<button data-view="insights" aria-label="Insights anzeigen">📊 Insights</button>
</nav>
</div>
<div class="theme-btns">
<button data-theme-value="light" aria-label="Helles Design">☀️</button>
<button data-theme-value="dark" aria-label="Dunkles Design">🌙</button>
<button data-theme-value="system" aria-label="System-Design" class="selected">📱</button>
</div>
</aside>
<main class="content" role="main">
<section id="board-section" class="section visible" aria-label="Board-Ansicht">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<h2 style="margin:0">Board</h2>
<button class="button primary" id="btn-new" aria-label="Neues Task erstellen">+ Neues Task</button>
</div>
<div class="toolbar" id="toolbar">
<input type="text" id="query" placeholder="Suchen..." aria-label="Tasks durchsuchen">
<select id="priority-filter" aria-label="Nach Priorität filtern">
<option value="">Alle Prioritäten</option>
<option value="Tief">Tief</option>
<option value="Mittel">Mittel</option>
<option value="Hoch">Hoch</option>
</select>
</div>
<div class="board" id="board" aria-label="Task-Board"></div>
</section>
<section id="streaks-section" class="section" aria-label="Streak-Ansicht">
<h2>Streaks</h2>
<div id="streak-grid"></div>
</section>
<section id="insights-section" class="section" aria-label="Insights-Ansicht">
<h2>Insights</h2>
<div class="stats" id="insights-stats"></div>
<div class="chart">
<div class="chart-title">Trend (Wochen)</div>
<div class="chart-canvas-wrap"><canvas id="trend-chart"></canvas></div>
</div>
<div class="chart">
<div class="chart-title">Prioritätsverteilung</div>
<div class="chart-canvas-wrap"><canvas id="donut-chart"></canvas></div>
<div class="chart-legend" id="donut-legend"></div>
</div>
</section>
</main>
</div>
<div class="modal-background" id="modal-bg" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal">
<h3 id="modal-title">Neues Task</h3>
<div class="field">
<label for="f-title">Titel</label>
<input type="text" id="f-title" maxlength="200" required>
</div>
<div class="field">
<label for="f-desc">Beschreibung</label>
<textarea id="f-desc"></textarea>
</div>
<div class="field">
<label for="f-priority">Priorität</label>
<select id="f-priority">
<option>Hoch</option>
<option>Mittel</option>
<option>Tief</option>
</select>
</div>
<div class="field">
<label for="f-status">Status</label>
<select id="f-status"></select>
</div>
<div class="field">
<label for="f-due">Fällig</label>
<input type="date" id="f-due">
</div>
<div class="modal-actions">
<div><button class="button secondary" id="f-delete" style="display:none">Löschen</button></div>
<div>
<button class="button secondary" id="f-cancel">Abbrechen</button>
<button class="button primary" id="f-save">Speichern</button>
</div>
</div>
</div>
</div>
<div class="palette-background" id="pal-bg" role="dialog" aria-modal="true" aria-label="Befehlspalette">
<div class="palette">
<input id="pal-q" placeholder="Befehl suchen..." aria-label="Suche">
<div class="palette-list" id="pal-list" role="listbox" aria-label="Verfügbare Befehle"></div>
</div>
</div>
<div class="toast-container" id="toasts" aria-live="polite"></div>
<div class="sr-only" id="live-region" aria-live="assertive" aria-atomic="true"></div>
<input type="file" id="imp" accept=".json" style="display:none">
<script>
(function () {
'use strict';
// === KONSTANTEN ===
const COLUMNS = [
['Backlog', 'backlog', '📋'],
['Aktiv', 'aktiv', '🟢'],
['Review', 'review', '🔄'],
['Fertig', 'fertig', '✅'],
];
// === STATE ===
const tasks = [];
const habits = [];
let currentTheme = 'system';
let currentView = 'board';
let previousFocus = null;
let paletteCommand = [];
let paletteIndex = 0;
let isEditing = false;
let dragSourceId = null;
// === HILFSFUNKTIONEN ===
function generateUniqueId() {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).slice(2, 6);
return 'd' + timestamp + '_' + random;
}
function htmlEscape(text) {
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function formatDateString(isoDateString) {
if (!isoDateString) return '';
try {
return new Date(isoDateString).toLocaleDateString('de-DE');
} catch (error) {
return '';
}
}
function showNotification(message, type) {
const container = document.getElementById('toasts');
if (!container) return;
const alertElement = document.createElement('div');
const typeClass = type === 'ok' ? 'success' : type === 'err' ? 'error' : 'info';
alertElement.className = 'toast ' + typeClass;
alertElement.textContent = message;
container.appendChild(alertElement);
setTimeout(function () {
if (alertElement.parentNode) {
alertElement.parentNode.removeChild(alertElement);
}
}, 2600);
}
function announceStatus(message) {
const liveRegion = document.getElementById('live-region');
if (liveRegion) {
liveRegion.textContent = message;
}
}
// === PERSISTENZ ===
function saveState() {
try {
localStorage.setItem('nv_t', JSON.stringify(tasks));
localStorage.setItem('nv_h', JSON.stringify(habits));
localStorage.setItem('nv_th', currentTheme);
} catch (error) {
// localStorage nicht verfügbar
}
}
function loadState() {
try {
const rawTasks = localStorage.getItem('nv_t');
const rawHabits = localStorage.getItem('nv_h');
const rawTheme = localStorage.getItem('nv_th');
if (rawTasks) {
const parsed = JSON.parse(rawTasks);
if (Array.isArray(parsed)) {
tasks.splice(0, tasks.length, ...parsed);
}
}
if (rawHabits) {
const parsed = JSON.parse(rawHabits);
if (Array.isArray(parsed)) {
habits.splice(0, habits.length, ...parsed);
}
}
if (rawTheme) {
currentTheme = rawTheme;
}
} catch (error) {
tasks.length = 0;
habits.length = 0;
currentTheme = 'system';
}
registerPalette();
}
// === THEME ===
function applyTheme() {
const mediaQuery = window.matchMedia('(prefers-color-scheme:dark)');
const documentElement = document.documentElement;
if (currentTheme === 'system') {
documentElement.setAttribute('data-theme', mediaQuery.matches ? 'dark' : 'light');
} else {
documentElement.setAttribute('data-theme', currentTheme);
}
const themeButtons = document.querySelectorAll('.theme-btns button');
for (const button of themeButtons) {
const themeValue = button.getAttribute('data-theme-value');
button.classList.toggle('selected', themeValue === currentTheme);
}
}
function subscribeToSystemThemeChange() {
const mediaQuery = window.matchMedia('(prefers-color-scheme:dark)');
mediaQuery.addEventListener('change', function () {
if (currentTheme === 'system') {
applyTheme();
}
});
}
// === NAVIGATION ===
function switchView(viewName) {
currentView = viewName;
location.hash = '#/' + viewName;
const navButtons = document.querySelectorAll('.sidebar nav button');
for (const button of navButtons) {
button.classList.toggle('selected', button.getAttribute('data-view') === viewName);
}
const sections = document.querySelectorAll('.section');
for (const section of sections) {
const isVisible = section.id === viewName + '-section';
section.classList.toggle('visible', isVisible);
}
renderView();
}
function renderView() {
if (currentView === 'board') {
renderBoard();
} else if (currentView === 'streaks') {
renderStreaks();
} else if (currentView === 'insights') {
renderInsights();
}
}
function clickOnTab(boardName) {
switchView(boardName);
}
// === EVENT HOKS ===
function bindBoardEvents() {
const boardSection = document.getElementById('board-section');
boardSection.addEventListener('click', function (event) {
const clickedTask = event.target.closest('.task');
if (!clickedTask) return;
const taskId = clickedTask.getAttribute('data-id');
const foundTask = tasks.find(function (t) { return t.id === taskId; });
if (foundTask) {
openTaskModal(foundTask);
}
});
boardSection.addEventListener('keydown', function (event) {
const focusedTask = document.querySelector('.task:focus, .task.selected');
if (!focusedTask) return;
const taskId = focusedTask.getAttribute('data-id');
const task = tasks.find(function (t) { return t.id === taskId; });
if (!task) return;
if (event.key === 'Delete') {
event.preventDefault();
const confirmed = confirm('Task wirklich löschen?');
if (!confirmed) return;
const index = tasks.indexOf(task);
if (index !== -1) {
tasks.splice(index, 1);
}
saveState();
renderBoard();
showNotification('Gelöscht', 'err');
return;
}
if (event.ctrlKey && event.altKey) {
event.preventDefault();
moveTaskByKeyboard(taskId, event.key, task.status);
}
});
boardSection.addEventListener('dragstart', onTaskDragStart);
boardSection.addEventListener('dragover', onBoardDragOver);
boardSection.addEventListener('dragenter', onBoardDragEnter);
boardSection.addEventListener('dragleave', onBoardDragLeave);
boardSection.addEventListener('drop', onBoardDrop);
}
let boardEventsBound = false;
if (!boardEventsBound) {
bindBoardEvents();
boardEventsBound = true;
}
// === BOARD RENDERING ===
function renderBoard() {
const query = document.getElementById('query').value.toLowerCase();
const priorityFilter = document.getElementById('priority-filter').value;
let boardContent = '';
for (const column of COLUMNS) {
const columnId = column[1];
const filteredTasks = tasks.filter(function (task) {
if (task.status !== columnId) return false;
if (query && task.title.toLowerCase().indexOf(query) === -1) return false;
if (priorityFilter && task.priority !== priorityFilter) return false;
return true;
});
let taskHtml = '';
for (const task of filteredTasks) {
const priorityClass = task.priority === 'Hoch' ? 'high' : task.priority === 'Tief' ? 'low' : 'medium';
const hasDate = task.dueDate ? formatDateString(task.dueDate) : '';
taskHtml += '<div class="task" data-id="' + htmlEscape(task.id) +
'" draggable="true" tabindex="0" role="button" aria-label="Task ' + htmlEscape(task.title) + '">' +
'<h3>' + htmlEscape(task.title) + '</h3>' +
(task.description ? '<p>' + htmlEscape(task.description) + '</p>' : '') +
'<div class="meta">' +
'<span class="badge ' + priorityClass + '">' + htmlEscape(task.priority) + '</span>' +
(hasDate ? '<span>' + htmlEscape(hasDate) + '</span>' : '') +
'</div></div>';
}
boardContent += '<div class="column" data-status="' + columnId + '"><div class="column-head">' +
'<span>' + column[2] + ' ' + column[0] + '</span>' +
'<span class="task-count">' + filteredTasks.length + '</span></div>' +
taskHtml + '</div>';
}
document.getElementById('board').innerHTML = boardContent;
}
// === TASK MODAL ===
function openTaskModal(task) {
previousFocus = document.activeElement;
isEditing = !!task;
const title = task ? task.title : '';
const description = task ? task.description : '';
const priority = task ? task.priority : 'Mittel';
const status = task ? task.status : 'aktiv';
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 = dueField;
document.getElementById('f-delete').style.display = isEditing ? 'inline-flex' : 'none';
let optionsHtml = '';
for (const column of COLUMNS) {
optionsHtml += '<option' + (status === column[1] ? ' selected' : '') + '>' + htmlEscape(column[0]) + '</option>';
}
document.getElementById('f-status').innerHTML = optionsHtml;
document.getElementById('modal-bg').classList.add('open');
document.getElementById('f-title').focus();
bindModalHandlers(task);
}
function bindModalHandlers(task) {
const saveButton = document.getElementById('f-save');
const cancelButton = document.getElementById('f-cancel');
const deleteButton = document.getElementById('f-delete');
saveButton.replaceWith(saveButton.cloneNode(true));
cancelButton.replaceWith(cancelButton.cloneNode(true));
deleteButton.replaceWith(deleteButton.cloneNode(true));
const newSave = document.getElementById('f-save');
const newCancel = document.getElementById('f-cancel');
const newDelete = document.getElementById('f-delete');
newSave.addEventListener('click', function () {
const newTitle = document.getElementById('f-title').value.trim();
if (!newTitle) {
showNotification('Bitte Titel eingeben', 'err');
document.getElementById('f-title').focus();
return;
}
const newDescription = document.getElementById('f-desc').value.trim();
const newPriority = document.getElementById('f-priority').value;
const newStatus = document.getElementById('f-status').value;
const newDueDate = document.getElementById('f-due').value
? new Date(document.getElementById('f-due').value + 'T00:00:00').toISOString()
: null;
if (isEditing) {
const existing = tasks.find(function (t) { return t.id === task.id; });
if (existing) {
existing.title = newTitle;
existing.description = newDescription;
existing.priority = newPriority;
existing.status = newStatus;
existing.dueDate = newDueDate;
if (newStatus === 'fertig' && !existing.finishedAt) {
existing.finishedAt = Date.now();
}
}
} else {
tasks.push({
id: generateUniqueId(),
title: newTitle,
description: newDescription,
priority: newPriority,
dueDate: newDueDate,
createdAt: Date.now(),
status: newStatus,
finishedAt: newStatus === 'fertig' ? Date.now() : null,
});
}
saveState();
renderView();
closeTaskModal();
showNotification('Gespeichert', 'grn');
});
newCancel.addEventListener('click', closeTaskModal);
newDelete.addEventListener('click', function () {
if (!confirm('Task wirklich löschen?')) return;
const indexOf = tasks.findIndex(function (t) { return t.id === task.id; });
if (indexOf !== -1) tasks.splice(indexOf, 1);
saveState();
renderView();
closeTaskModal();
showNotification('Gelöscht', 'err');
});
}
function closeTaskModal() {
document.getElementById('modal-bg').classList.remove('open');
if (previousFocus) {
try { previousFocus.focus(); } catch (e) { }
}
}
// === STREAKS ===
function renderStreaks() {
const gridContainer = document.getElementById('streak-grid');
if (habits.length === 0) {
gridContainer.innerHTML = '<p>Keine Habits. Es gibt keine Streak-Daten.</p>';
return;
}
let html = '';
for (const habit of habits) {
const currentStreak = calculateCurrentStreak(habit);
const longestStreak = calculateLongestStreak(habit);
html += '<div class="streak"><div class="streak-header"><h3>' +
htmlEscape(habit.name) +
'</h3><div><span class="streak-current">Aktuell: ' + currentStreak + '</span> | ' +
'<span class="streak-longest">Längste: ' + longestStreak + '</span></div></div>' +
'<div class="streak-grid">';
for (let i = 0; i < 26; i++) {
const day = new Date();
day.setDate(day.getDate() - i);
const dateKey = day.toISOString().slice(0, 10);
const value = habit.values ? habit.values[dateKey] || 0 : 0;
html += '<div class="streak-cell" data-value="' + value + '" data-date="' + dateKey + '" tabindex="0" role="button" aria-label="' + formatDateString(dateKey) + ', Level ' + value + '"></div>';
}
html += '</div></div>';
}
gridContainer.innerHTML = html;
gridContainer.addEventListener('click', function (event) {
const cell = event.target.closest('.streak-cell');
if (!cell) return;
const currentValue = parseInt(cell.getAttribute('data-value'), 10);
const nextValue = (currentValue + 1) % 5;
cell.setAttribute('data-value', nextValue);
const habitName = cell.closest('.streak').querySelector('h3').textContent;
const habit = habits.find(function (h) { return h.name === habitName; });
if (habit && !habit.values) habit.values = {};
if (habit) {
habit.values[cell.getAttribute('data-date')] = nextValue;
saveState();
renderStreaks();
}
});
}
function calculateCurrentStreak(habit) {
if (!habit.values) return 0;
let streak = 0;
const today = new Date();
for (let i = 0; i < 90; i++) {
const day = new Date(today);
day.setDate(day.getDate() - i);
const key = day.toISOString().slice(0, 10);
const val = habit.values[key];
if (val === undefined || val === 0) break;
streak++;
}
return streak;
}
function calculateLongestStreak(habit) {
if (!habit.values) return 0;
let longest = 0;
let current = 0;
const today = new Date();
for (let i = 0; i < 365; i++) {
const day = new Date(today);
day.setDate(day.getDate() - i);
const key = day.toISOString().slice(0, 10);
const val = habit.values[key];
if (val > 0) {
current++;
longest = Math.max(longest, current);
} else {
current = 0;
}
}
return longest;
}
// === INSIGHTS (2 Charts) ===
function renderInsights() {
const totalTasks = tasks.length;
let completed = 0;
let active = 0;
let overdue = 0;
for (const task of tasks) {
if (task.status === 'fertig') completed++;
else if (task.status === 'aktiv') active++;
else if (task.dueDate && new Date(task.dueDate) < new Date()) overdue++;
}
document.getElementById('insights-stats').innerHTML =
'<div class="stat-card"><div class="value">' + totalTasks + '</div><div class="label">Alle</div></div>' +
'<div class="stat-card"><div class="value" style="color:var(--green)">' + completed + '</div><div class="label">Erledigt</div></div>' +
'<div class="stat-card"><div class="value">' + active + '</div><div class="label">Aktiv</div></div>' +
'<div class="stat-card"><div class="value" style="color:var(--red)">' + overdue + '</div><div class="label">Überfällig</div></div>';
drawTrendChart();
drawDonutChart();
}
function drawTrendChart() {
const canvas = document.getElementById('trend-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.offsetWidth || 700;
canvas.height = 200;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const now = new Date();
const weekData = [];
for (let w = 0; w < 12; w++) {
const weekStart = new Date(now);
weekStart.setDate(weekStart.getDate() - (w * 7) - weekStart.getDay() + 1);
weekStart.setHours(0, 0, 0, 0);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
let count = 0;
for (const task of tasks) {
if (!task.finishedAt) continue;
const finished = new Date(task.finishedAt);
if (finished >= weekStart && finished <= weekEnd) {
count++;
}
}
const label = (weekStart.getMonth() + 1) + '/' + w;
weekData.push({ label: label, count: count });
}
weekData.reverse();
const maxCount = Math.max(1, ...weekData.map(function (d) { return d.count; }));
const marginLeft = 35, marginRight = 10, marginTop = 10, marginBottom = 30;
const graphWidth = canvas.width - marginLeft - marginRight;
const graphHeight = canvas.height - marginTop - marginBottom;
const barWidth = 30;
const gap = (graphWidth / weekData.length - barWidth) * 0.5;
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = marginTop + graphHeight - (i / 4) * graphHeight;
ctx.beginPath();
ctx.moveTo(marginLeft, y);
ctx.lineTo(canvas.width - marginRight, y);
ctx.stroke();
ctx.fillStyle = '#6e6e73';
ctx.font = '10px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(Math.round(maxCount / 4 * i), marginLeft - 5, y + 4);
}
ctx.fillStyle = '#0071e3';
for (let i = 0; i < weekData.length; i++) {
const x = marginLeft + i * (barWidth / 1.5 + gap / 2);
const height = Math.max((weekData[i].count / maxCount) * graphHeight, 1);
const y = marginTop + graphHeight - height;
ctx.fillRect(x, y, barWidth, height);
ctx.fillStyle = '#6e6e73';
ctx.textAlign = 'center';
ctx.font = '9px sans-serif';
ctx.fillText(weekData[i].label, x + barWidth / 2, canvas.height - marginBottom + 12);
if (weekData[i].count > 0) {
ctx.fillStyle = '#1d1d1f';
ctx.fillText(weekData[i].count, x + barWidth / 2, y - 4);
}
}
}
function drawDonutChart() {
const canvas = document.getElementById('donut-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.offsetWidth || 400;
canvas.height = 200;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const counts = { 'Hoch': 0, 'Mittel': 0, 'Tief': 0 };
for (const task of tasks) {
if (counts[task.priority] !== undefined) {
counts[task.priority]++;
}
}
const total = tasks.length || 1;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY) - 20;
const innerRadius = radius * 0.6;
const colors = { 'Hoch': '#ff3b30', 'Mittel': '#ffcc00', 'Tief': '#34c759' };
let startAngle = -Math.PI / 2;
for (const key of Object.keys(counts)) {
const slice = (counts[key] / total) * 2 * Math.PI;
if (slice > 0) {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startAngle, startAngle + slice);
ctx.arc(centerX, centerY, innerRadius, startAngle + slice, startAngle, true);
ctx.closePath();
ctx.fillStyle = colors[key];
ctx.fill();
}
startAngle += slice;
}
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(centerX, centerY, innerRadius, 0, Math.PI * 2);
ctx.fill();
const legendEl = document.getElementById('donut-legend');
if (legendEl) {
legendEl.innerHTML = '';
for (const key of Object.keys(counts)) {
const legendItem = document.createElement('span');
legendItem.innerHTML = '<span class="color-dot" style="background:' + colors[key] + '"></span> ' + htmlEscape(key) + ' (' + counts[key] + ')';
legendEl.appendChild(legendItem);
}
}
}
// === COMMAND PALETTE ===
function registerPalette() {
const commands = [
{ name: '📅 Board', shortcut: 'Cmd+K→B', action: function () { switchView('board'); } },
{ name: '🔥 Streaks', shortcut: 'Cmd+K→S', action: function () { switchView('streaks'); } },
{ name: '📊 Insights', shortcut: 'Cmd+K→I', action: function () { switchView('insights'); } },
{ name: ' Neues Task', shortcut: 'Cmd+K→N', action: function () { openTaskModal(null); } },
{ name: '📥 Importieren', shortcut: 'Cmd+K→M', action: function () { document.getElementById('imp').click(); } },
{ name: '📤 Exportieren', shortcut: 'Cmd+K→E', action: function () { doExport(); } },
{ name: '🗑️ Demo löschen', shortcut: 'Cmd+K→R', action: function () {
tasks.length = 0; habits.length = 0; saveState(); renderView(); showNotification('Zurückgesetzt', 'blu');
} },
{ name: '⚡ 500 Tasks generieren', shortcut: 'Cmd+K→5', action: function () { generate500Tasks(); } },
];
const recentTasks = tasks.slice(-5).reverse();
for (const task of recentTasks) {
commands.push({
name: '✏️ ' + task.title,
shortcut: '',
action: function (t) { openTaskModal(t); }.bind(null, task),
});
}
paletteCommand = commands;
}
function buildPaletteList() {
paletteIndex = 0;
let html = '';
for (let i = 0; i < paletteCommand.length; i++) {
html += '<div class="palette-item" data-index="' + i + '">' +
'<span class="name">' + htmlEscape(paletteCommand[i].name) +
'</span><span class="shortcut">' + htmlEscape(paletteCommand[i].shortcut) + '</span></div>';
}
document.getElementById('pal-list').innerHTML = html;
const items = document.querySelectorAll('#pal-list .palette-item');
for (const item of items) {
item.addEventListener('mouseenter', function () {
paletteIndex = parseInt(this.getAttribute('data-index'), 10);
highlightPaletteIndex();
});
item.addEventListener('click', function () {
const command = paletteCommand[parseInt(this.getAttribute('data-index'), 10)];
if (command) command.action();
});
}
}
function highlightPaletteIndex() {
const items = document.querySelectorAll('#pal-list .palette-item');
for (let i = 0; i < items.length; i++) {
items[i].classList.toggle('selected', i === paletteIndex);
}
}
function filterPaletteItems() {
const query = document.getElementById('pal-q').value.toLowerCase();
const items = document.querySelectorAll('#pal-list .palette-item');
for (const item of items) {
const name = item.querySelector('.name').textContent.toLowerCase();
item.style.display = name.indexOf(query) === -1 ? 'none' : 'flex';
}
}
function openPalette() {
document.getElementById('pal-bg').classList.add('open');
document.getElementById('pal-q').value = '';
document.getElementById('pal-q').focus();
registerPalette();
buildPaletteList();
}
function closePalette() {
document.getElementById('pal-bg').classList.remove('open');
}
document.querySelector('.palette-background').addEventListener('click', function (event) {
if (event.target.id === 'pal-bg') {
closePalette();
}
});
document.addEventListener('keydown', function (event) {
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault();
openPalette();
return;
}
if (event.key === 'Escape') {
closeTaskModal();
setTimeout(closePalette, 50);
}
});
document.getElementById('pal-q').addEventListener('input', function () {
buildPaletteList();
filterPaletteItems();
});
document.getElementById('pal-q').addEventListener('keydown', function (event) {
const visibleItems = document.querySelectorAll('#pal-list .palette-item:not([style*="display: none"])');
if (!visibleItems.length) return;
if (event.key === 'ArrowDown') {
event.preventDefault();
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 next = pos >= indices.length - 1 ? indices[0] : indices[pos + 1];
paletteIndex = next;
highlightPaletteIndex();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
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();
} else if (event.key === 'Escape') {
closePalette();
} else {
return;
}
});
// === DRAG & DROP ===
function onTaskDragStart(event) {
const task = event.target.closest('.task');
if (!task) return;
event.stopPropagation();
dragSourceId = task.getAttribute('data-id');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', dragSourceId);
task.classList.add('dragging');
event.target.addEventListener('dragend', onTaskDragEnd);
}
function onTaskDragEnd(event) {
const task = event.target;
task.classList.remove('dragging');
task.removeEventListener('dragend', onTaskDragEnd);
}
function onBoardDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
function onBoardDragEnter(event) {
event.preventDefault();
const column = event.target.closest('.column');
if (column) column.classList.add('drag-over');
}
function onBoardDragLeave(event) {
const column = event.target.closest('.column');
if (column && !column.contains(event.relatedTarget)) {
column.classList.remove('drag-over');
}
}
function onBoardDrop(event) {
event.preventDefault();
dragSourceId = event.dataTransfer.getData('text/plain');
document.querySelectorAll('.column').forEach(function (col) {
col.classList.remove('drag-over');
});
if (!dragSourceId) return;
const task = tasks.find(function (t) { return t.id === dragSourceId; });
if (!task) return;
const targetColumn = event.target.closest('.column');
if (!targetColumn) return;
const targetStatus = targetColumn.getAttribute('data-status');
if (targetStatus === task.status) {
const targetTask = event.target.closest('.task');
if (targetTask && targetTask.getAttribute('data-id')) {
const targetId = targetTask.getAttribute('data-id');
if (targetId === task.id) return;
const fromIndex = tasks.findIndex(function (t) { return t.id === targetId; });
const toIndex = tasks.findIndex(function (t) { return t.id === task.id; });
if (fromIndex !== -1 && toIndex !== -1 && fromIndex > toIndex) {
tasks.splice(toIndex, 1);
tasks.splice(fromIndex, 0, task);
}
}
} else {
const fromIndex = tasks.indexOf(task);
if (fromIndex !== -1) tasks.splice(fromIndex, 1);
task.status = targetStatus;
if (targetStatus === 'fertig') task.finishedAt = Date.now();
tasks.push(task);
}
saveState();
renderView();
showNotification('Gezogen zu ' + targetStatus, 'grn');
}
// === TASTATUR-DAG & DROP ===
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') {
const newStatusIndex = (arrowKey === 'ArrowRight')
? Math.min(columnIndex + 1, COLUMNS.length - 1)
: Math.max(columnIndex - 1, 0);
task.status = COLUMNS[newStatusIndex][1];
if (task.status === 'fertig') task.finishedAt = Date.now();
else task.finishedAt = null;
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') {
const spaltenTasks = tasks.filter(function (t) { return t.status === currentStatus; });
const currentIndex = spaltenTasks.findIndex(function (t) { return t.id === taskId; });
const newIndex = (arrowKey === 'ArrowDown')
? Math.min(currentIndex + 1, spaltenTasks.length - 1)
: Math.max(currentIndex - 1, 0);
const targetId = spaltenTasks[newIndex].id;
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);
const newFocused = document.querySelector('.task[data-id="' + taskId + '"]');
if (newFocused) newFocused.focus();
}
saveState();
renderView();
announceStatus('Task verschoben');
}
// === EXPORT / IMPORT ===
function doExport() {
const exportData = {
version: 1,
tasks: tasks,
habits: habits,
theme: currentTheme,
exportedAt: new Date().toISOString(),
};
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = 'nova-export-' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
showNotification('Export OK', 'grn');
}
function doImport(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = function (event) {
try {
const imported = JSON.parse(event.target.result);
if (!imported || typeof imported !== 'object') {
throw new Error('keine JSON-Struktur');
}
if (imported.version !== 1 && imported.version !== undefined) {
// Alte Version, akzeptiert
}
if (!imported.tasks || !Array.isArray(imported.tasks)) {
throw new Error('tasks fehlt oder ist kein Array');
}
const validTasks = [];
const invalidCount = [];
for (const task of imported.tasks) {
if (!task.id || !task.title || !task.status) {
invalidCount.push(task.title || task.id);
continue;
}
validTasks.push({
id: task.id,
title: task.title,
description: task.description || '',
priority: task.priority || 'Mittel',
status: task.status,
dueDate: task.dueDate || task.d || null,
createdAt: task.createdAt || task.dt || Date.now(),
finishedAt: task.finishedAt || task.df || null,
});
}
if (validTasks.length === 0) {
throw new Error('Keine gültigen Tasks gefunden');
}
tasks.splice(0, tasks.length, ...validTasks);
if (imported.habits && Array.isArray(imported.habits)) {
habits.splice(0, habits.length, ...imported.habits);
}
if (imported.theme) {
currentTheme = imported.theme;
applyTheme();
}
saveState();
registerPalette();
renderView();
showNotification('Import OK (' + validTasks.length + ' Tasks, ' + invalidCount.length + ' ignoriert)', 'grn');
if (invalidCount.length > 0) {
alert('Einige Tasks wurden nicht importiert (fehlende id/Title/Status): ' + invalidCount.join(', '));
}
} catch (error) {
showNotification('Import fehlgeschlagen: ' + error.message, 'err');
}
};
reader.readAsText(file);
}
document.getElementById('imp').addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) doImport(file);
event.target.value = '';
});
// === DEMO-DATEN ===
let demoLoaded = false;
function loadDemoData() {
if (demoLoaded && tasks.length > 0) return;
demoLoaded = true;
const titles = [
'Dashboard', 'API', 'Cache', 'Logging', 'Tests', 'Docs', 'CI/CD', 'Auth', 'Login', 'Panel',
'Widget', 'Chart', 'Setup', 'Deploy', 'Monitor', 'Alert', 'Backup', 'Config', 'Schema', 'Migration',
'Refactor', 'Bugfix', 'Feature', 'Story', 'Epic', 'Release', 'Hotfix', 'Review', 'Sprint', 'Retro',
];
for (let i = 0; i < 40; i++) {
const status = ['backlog', 'aktiv', 'review', 'fertig'][i % 4];
const priority = ['Hoch', 'Tief', 'Mittel', 'Mittel', 'Tief'][i % 5];
const dueDate = new Date(Date.now() + i * 864e5).toISOString();
tasks.push({
id: generateUniqueId(),
title: titles[i] + ' #' + (i + 1),
description: 'Beschreibung für ' + titles[i] + ' #' + (i + 1),
priority: priority,
dueDate: dueDate,
createdAt: Date.now() - i * 864e5,
status: status,
finishedAt: status === 'fertig' ? Date.now() : null,
});
}
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();
}
function generateHabitValues(days) {
const values = {};
for (let i = 0; i < days; i++) {
const day = new Date();
day.setDate(day.getDate() - i);
values[day.toISOString().slice(0, 10)] = Math.random() < 0.65 ? Math.floor(Math.random() * 4) + 1 : 0;
}
return values;
}
// === 500 TASKS GENERIEREN (Performance: Batch) ===
function generate500Tasks() {
const batch = [];
for (let i = 0; i < 500; i++) {
const status = ['backlog', 'aktiv', 'review', 'fertig'][i % 4];
const priority = ['Hoch', 'Tief', 'Mittel', 'Mittel', 'Tief'][i % 5];
const title = 'Batch Task #' + (i + 1);
batch.push({
id: generateUniqueId(),
title: title,
description: 'Automatisch generierter Task #' + (i + 1),
priority: priority,
dueDate: new Date(Date.now() + i * 1728e5).toISOString(),
createdAt: Date.now() - i * 1728e5,
status: status,
finishedAt: status === 'fertig' ? Date.now() : null,
});
}
tasks.splice(0, 0, ...batch);
saveState();
renderView();
showNotification('500 Tasks generiert', 'grn');
}
// === HASH-ROUTING ===
function setupHashRouting() {
const views = ['board', 'streaks', 'insights'];
function handleHashChange() {
const hash = location.hash.replace('#/', '');
if (views.includes(hash)) {
currentView = hash;
const navButtons = document.querySelectorAll('.sidebar nav button');
for (const button of navButtons) {
button.classList.toggle('selected', button.getAttribute('data-view') === currentView);
}
const sections = document.querySelectorAll('.section');
for (const section of sections) {
section.classList.toggle('visible', section.id === currentView + '-section');
}
renderView();
}
}
window.addEventListener('hashchange', handleHashChange);
handleHashChange();
}
setupHashRouting();
// === SIDEBAR-CLICK EVENTS ===
document.querySelectorAll('.sidebar button[data-view]').forEach(function (button) {
button.addEventListener('click', function () {
const viewName = button.getAttribute('data-view');
switchView(viewName);
});
});
document.querySelectorAll('.theme-btns button').forEach(function (button) {
button.addEventListener('click', function () {
currentTheme = button.getAttribute('data-theme-value');
applyTheme();
saveState();
renderView();
});
});
document.getElementById('btn-new').addEventListener('click', function () {
openTaskModal(null);
});
const queryInput = document.getElementById('query');
const prioritySelect = document.getElementById('priority-filter');
let queryRenderTimeout = null;
queryInput.addEventListener('input', function () {
clearTimeout(queryRenderTimeout);
queryRenderTimeout = setTimeout(renderBoard, 150);
});
prioritySelect.addEventListener('change', renderBoard);
// === APP START ===
loadState();
applyTheme();
subscribeToSystemThemeChange();
if (tasks.length === 0 && habits.length === 0) {
loadDemoData();
}
renderView();
})();
</script>
</body>
</html>