diff --git a/api.html b/api.html
index 499f0b9..26adbd4 100644
--- a/api.html
+++ b/api.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -132,7 +133,8 @@ fetch('https://jsonplaceholder.typicode.com/users')
© 2026 β Made with π & β¨ β David & Karo
diff --git a/css/style.css b/css/style.css
index c09542e..da04451 100644
--- a/css/style.css
+++ b/css/style.css
@@ -3187,3 +3187,393 @@ body.dark .api-user-details a { color: #f9a8d4; }
#api-search { flex: unset; width: 100%; }
.user-list { grid-template-columns: 1fr; }
}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Kanban-Board β kanban.html
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+/* Haupt-Layout */
+.kanban-main {
+ padding: 1.5rem 1rem 3rem;
+ max-width: 100%;
+ overflow-x: hidden;
+}
+
+/* Header-Leiste */
+.kanban-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.kanban-title h1 {
+ margin: 0 0 0.2rem;
+}
+
+.kanban-title p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #888;
+}
+
+/* Toolbar */
+.kanban-toolbar {
+ display: flex;
+ gap: 0.6rem;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.btn-kanban {
+ padding: 0.5rem 1rem;
+ border-radius: 8px;
+ font-size: 0.88rem;
+ font-weight: 600;
+ border: none;
+ cursor: pointer;
+ transition: filter 0.15s, transform 0.12s;
+ background: linear-gradient(135deg, #ec4899, #be185d);
+ color: #fff;
+}
+
+.btn-kanban:hover { filter: brightness(1.1); transform: translateY(-1px); }
+.btn-kanban:active { transform: translateY(0); }
+
+.btn-kanban.btn-secondary {
+ background: linear-gradient(135deg, #a78bfa, #7c3aed);
+}
+
+.btn-kanban.btn-danger {
+ background: linear-gradient(135deg, #f87171, #dc2626);
+}
+
+/* Board (horizontaler Scroll) */
+.kanban-board {
+ display: flex;
+ gap: 1.25rem;
+ align-items: flex-start;
+ overflow-x: auto;
+ padding-bottom: 1rem;
+ min-height: 60vh;
+ /* custom scrollbar */
+ scrollbar-width: thin;
+ scrollbar-color: #f9a8d4 #fce7f3;
+}
+
+.kanban-board::-webkit-scrollbar { height: 8px; }
+.kanban-board::-webkit-scrollbar-track { background: #fce7f3; border-radius: 4px; }
+.kanban-board::-webkit-scrollbar-thumb { background: #f9a8d4; border-radius: 4px; }
+
+/* Einzelne Spalte */
+.kanban-column {
+ background: #fdf2f8;
+ border: 2px solid #f9a8d4;
+ border-radius: 16px;
+ width: 280px;
+ min-width: 260px;
+ max-width: 300px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
+}
+
+.kanban-column.drag-over {
+ border-color: #ec4899;
+ background: #fce7f3;
+ box-shadow: 0 0 0 3px rgba(236,72,153,0.25);
+}
+
+/* Spalten-Header */
+.kanban-col-header {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.85rem 0.85rem 0.6rem;
+ border-bottom: 1px solid #f9a8d4;
+}
+
+.kanban-col-title {
+ flex: 1;
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: #be185d;
+ cursor: default;
+ outline: none;
+ border-radius: 4px;
+ padding: 2px 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.kanban-col-title[contenteditable="true"] {
+ cursor: text;
+ background: #fff;
+ outline: 2px solid #ec4899;
+ white-space: normal;
+ overflow: visible;
+ text-overflow: unset;
+}
+
+.kanban-col-badge {
+ background: #ec4899;
+ color: #fff;
+ font-size: 0.75rem;
+ font-weight: 700;
+ min-width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 5px;
+ flex-shrink: 0;
+}
+
+.kanban-col-delete {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 0.85rem;
+ color: #ccc;
+ padding: 2px 4px;
+ border-radius: 4px;
+ transition: color 0.15s, background 0.15s;
+ flex-shrink: 0;
+}
+
+.kanban-col-delete:hover { color: #dc2626; background: #fee2e2; }
+
+/* Karten-Liste */
+.kanban-card-list {
+ list-style: none;
+ padding: 0.6rem;
+ margin: 0;
+ flex: 1;
+ min-height: 60px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+}
+
+/* Drop-Placeholder */
+.kanban-drop-placeholder {
+ height: 56px;
+ border: 2px dashed #f9a8d4;
+ border-radius: 10px;
+ background: rgba(249,168,212,0.15);
+ flex-shrink: 0;
+}
+
+/* Karte */
+.kanban-card {
+ background: #fff;
+ border: 1px solid #fce7f3;
+ border-radius: 10px;
+ padding: 0;
+ overflow: hidden;
+ box-shadow: 0 2px 6px rgba(236,72,153,0.08);
+ cursor: grab;
+ transition: box-shadow 0.2s, transform 0.15s, opacity 0.15s;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.kanban-card:hover {
+ box-shadow: 0 5px 15px rgba(236,72,153,0.18);
+ transform: translateY(-2px);
+}
+
+.kanban-card.dragging {
+ opacity: 0.4;
+ cursor: grabbing;
+ transform: rotate(2deg) scale(1.02);
+}
+
+/* Label-Streifen oben */
+.kanban-card-label {
+ height: 6px;
+ border-radius: 10px 10px 0 0;
+ flex-shrink: 0;
+}
+
+/* Karten-Text (contenteditable) */
+.kanban-card-text {
+ padding: 0.6rem 2rem 0.6rem 0.7rem;
+ font-size: 0.9rem;
+ color: #333;
+ outline: none;
+ min-height: 2.2rem;
+ line-height: 1.5;
+ word-break: break-word;
+ cursor: text;
+}
+
+.kanban-card-text:focus {
+ background: #fff9fc;
+}
+
+/* LΓΆschen-Button */
+.kanban-card-delete {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ background: none;
+ border: none;
+ font-size: 0.75rem;
+ color: #ddd;
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 4px;
+ line-height: 1;
+ transition: color 0.15s, background 0.15s;
+ opacity: 0;
+}
+
+.kanban-card:hover .kanban-card-delete,
+.kanban-card-delete:focus {
+ opacity: 1;
+}
+
+.kanban-card-delete:hover { color: #dc2626; background: #fee2e2; }
+
+/* Spalten-Footer */
+.kanban-col-footer {
+ padding: 0.5rem 0.6rem 0.7rem;
+}
+
+.btn-add-card {
+ width: 100%;
+ padding: 0.45rem;
+ background: none;
+ border: 2px dashed #f9a8d4;
+ border-radius: 8px;
+ color: #be185d;
+ font-size: 0.88rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+}
+
+.btn-add-card:hover {
+ background: #fce7f3;
+ border-color: #ec4899;
+}
+
+/* ββ Label Picker (Kontext-MenΓΌ) ββ */
+.label-picker {
+ position: absolute;
+ z-index: 9999;
+ background: #fff;
+ border: 1px solid #f9a8d4;
+ border-radius: 12px;
+ padding: 0.75rem;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
+ min-width: 200px;
+}
+
+.label-picker[hidden] { display: none; }
+
+.label-picker-title {
+ margin: 0 0 0.5rem;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: #be185d;
+}
+
+.label-colors {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.label-swatch {
+ width: 30px;
+ height: 30px;
+ border: 2px solid rgba(0,0,0,0.1);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.12s, border-color 0.12s;
+ padding: 0;
+}
+
+.label-swatch:hover { transform: scale(1.2); border-color: rgba(0,0,0,0.3); }
+
+/* ββ Dark Mode ββ */
+body.dark .kanban-column {
+ background: #1a1a2e;
+ border-color: #7c3aed55;
+}
+
+body.dark .kanban-column.drag-over {
+ background: #1e1b3a;
+ border-color: #a78bfa;
+ box-shadow: 0 0 0 3px rgba(167,139,250,0.25);
+}
+
+body.dark .kanban-col-header {
+ border-bottom-color: #7c3aed44;
+}
+
+body.dark .kanban-col-title { color: #f9a8d4; }
+body.dark .kanban-col-delete:hover { background: #3d1515; }
+
+body.dark .kanban-card {
+ background: #16213e;
+ border-color: #7c3aed33;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
+}
+
+body.dark .kanban-card:hover {
+ box-shadow: 0 5px 15px rgba(167,139,250,0.2);
+}
+
+body.dark .kanban-card-text { color: #e0e0e0; }
+body.dark .kanban-card-text:focus { background: #1c1c2e; }
+
+body.dark .kanban-drop-placeholder {
+ border-color: #7c3aed88;
+ background: rgba(167,139,250,0.08);
+}
+
+body.dark .btn-add-card {
+ border-color: #7c3aed55;
+ color: #f9a8d4;
+}
+
+body.dark .btn-add-card:hover {
+ background: #1e1b3a;
+ border-color: #a78bfa;
+}
+
+body.dark .label-picker {
+ background: #1e1e2e;
+ border-color: #7c3aed55;
+ color: #f0f0f0;
+}
+
+body.dark .label-picker-title { color: #f9a8d4; }
+
+body.dark .kanban-title p { color: #777; }
+
+body.dark .kanban-board {
+ scrollbar-color: #7c3aed #1a1a2e;
+}
+
+/* ββ Responsive ββ */
+@media (max-width: 600px) {
+ .kanban-header { flex-direction: column; }
+ .kanban-toolbar { width: 100%; }
+ .btn-kanban { flex: 1; text-align: center; }
+ .kanban-column { width: 260px; min-width: 240px; }
+}
diff --git a/eis_projekt.html b/eis_projekt.html
index 7a0c2ad..4f751f0 100644
--- a/eis_projekt.html
+++ b/eis_projekt.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -177,6 +178,7 @@
diff --git a/galerie.html b/galerie.html
index 49014fe..cef09fd 100644
--- a/galerie.html
+++ b/galerie.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -89,6 +90,7 @@
diff --git a/impressum.html b/impressum.html
index b757496..342ca08 100644
--- a/impressum.html
+++ b/impressum.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -119,6 +120,7 @@
diff --git a/index.html b/index.html
index 106353a..91281d6 100644
--- a/index.html
+++ b/index.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -89,6 +90,7 @@
diff --git a/js/kanban.js b/js/kanban.js
new file mode 100644
index 0000000..bf2deac
--- /dev/null
+++ b/js/kanban.js
@@ -0,0 +1,503 @@
+/**
+ * kanban.js
+ * ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ * Voll funktionsfΓ€higes Kanban-Board (Trello-Style)
+ * β’ Spalten anlegen / umbenennen / lΓΆschen
+ * β’ Karten anlegen (contenteditable) / lΓΆschen
+ * β’ Drag & Drop zwischen Spalten + Reihenfolge innerhalb einer Spalte
+ * β’ Drop-Zonen-Visualisierung (dragenter / dragleave)
+ * β’ Farbige Labels per Rechtsklick-KontextmenΓΌ
+ * β’ VollstΓ€ndige Persistenz in localStorage
+ * β’ Export als JSON-Datei (Blob + createObjectURL)
+ * β’ Reset auf leeres Board
+ * ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ */
+
+(function () {
+ 'use strict';
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Konstanten & State
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const STORAGE_KEY = 'kanban_board_v2';
+
+ const DEFAULT_BOARD = {
+ columns: [
+ {
+ id: uid(),
+ title: 'π To Do',
+ cards: [
+ { id: uid(), text: 'Aufgabe 1 β Beispiel', label: '#60a5fa' },
+ { id: uid(), text: 'Aufgabe 2 β Beispiel', label: 'none' },
+ ],
+ },
+ {
+ id: uid(),
+ title: 'βοΈ In Arbeit',
+ cards: [
+ { id: uid(), text: 'fetch & API integrieren', label: '#a78bfa' },
+ ],
+ },
+ {
+ id: uid(),
+ title: 'β
Erledigt',
+ cards: [
+ { id: uid(), text: 'Dark Mode implementieren', label: '#4ade80' },
+ { id: uid(), text: 'Hamburger-MenΓΌ Mobile', label: '#4ade80' },
+ ],
+ },
+ ],
+ };
+
+ /* Laufender State (wird aus localStorage oder Default geladen) */
+ let state = loadState();
+
+ /* Drag-Kontext */
+ let dragCardId = null; // ID der gezogenen Karte
+ let dragColumnId = null; // Quell-Spalte
+
+ /* Label-Picker Kontext */
+ let labelTargetCardId = null;
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Utility
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function uid () {
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
+ }
+
+ function loadState () {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return JSON.parse(raw);
+ } catch (_) {}
+ // Deep-clone default so UIDs werden nur einmal erzeugt
+ return JSON.parse(JSON.stringify(DEFAULT_BOARD));
+ }
+
+ function saveState () {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ }
+
+ function findColumn (colId) {
+ return state.columns.find(c => c.id === colId);
+ }
+
+ function findCard (cardId) {
+ for (const col of state.columns) {
+ const card = col.cards.find(c => c.id === cardId);
+ if (card) return { card, column: col };
+ }
+ return null;
+ }
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Board rendern
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const board = document.getElementById('kanban-board');
+
+ function renderBoard () {
+ board.innerHTML = '';
+ state.columns.forEach(col => board.appendChild(buildColumn(col)));
+ // "Spalte hinzufΓΌgen"-Ghost am Ende (fΓΌr Drop-Hint, optional)
+ }
+
+ /* ββ Spalte bauen ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function buildColumn (colData) {
+ const col = document.createElement('div');
+ col.className = 'kanban-column';
+ col.dataset.colId = colData.id;
+
+ /* Header */
+ const header = document.createElement('div');
+ header.className = 'kanban-col-header';
+
+ const titleEl = document.createElement('h2');
+ titleEl.className = 'kanban-col-title';
+ titleEl.textContent = colData.title;
+ titleEl.title = 'Doppelklick zum Umbenennen';
+ titleEl.addEventListener('dblclick', () => renameColumn(colData.id, titleEl));
+
+ const badge = document.createElement('span');
+ badge.className = 'kanban-col-badge';
+ badge.textContent = colData.cards.length;
+
+ const deleteColBtn = document.createElement('button');
+ deleteColBtn.type = 'button';
+ deleteColBtn.className = 'kanban-col-delete';
+ deleteColBtn.title = 'Spalte lΓΆschen';
+ deleteColBtn.innerHTML = 'β';
+ deleteColBtn.addEventListener('click', () => deleteColumn(colData.id));
+
+ header.appendChild(titleEl);
+ header.appendChild(badge);
+ header.appendChild(deleteColBtn);
+
+ /* Karten-Liste */
+ const cardList = document.createElement('ul');
+ cardList.className = 'kanban-card-list';
+ cardList.dataset.colId = colData.id;
+
+ colData.cards.forEach(cardData => {
+ cardList.appendChild(buildCard(cardData, colData.id));
+ });
+
+ /* Drop-Placeholder (sichtbar beim Drag) */
+ const placeholder = document.createElement('li');
+ placeholder.className = 'kanban-drop-placeholder';
+ placeholder.dataset.placeholder = 'true';
+
+ /* Drag-Events auf cardList */
+ cardList.addEventListener('dragover', e => onCardListDragOver(e, cardList));
+ cardList.addEventListener('dragenter', e => onColumnDragEnter(e, col));
+ cardList.addEventListener('dragleave', e => onColumnDragLeave(e, col));
+ cardList.addEventListener('drop', e => onCardListDrop(e, colData.id, cardList));
+
+ /* Footer: + Karte Button */
+ const footer = document.createElement('div');
+ footer.className = 'kanban-col-footer';
+ const addCardBtn = document.createElement('button');
+ addCardBtn.type = 'button';
+ addCardBtn.className = 'btn-add-card';
+ addCardBtn.textContent = '+ Karte';
+ addCardBtn.addEventListener('click', () => addCard(colData.id, cardList, badge, colData));
+ footer.appendChild(addCardBtn);
+
+ col.appendChild(header);
+ col.appendChild(cardList);
+ col.appendChild(footer);
+ return col;
+ }
+
+ /* ββ Karte bauen βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function buildCard (cardData, colId) {
+ const li = document.createElement('li');
+ li.className = 'kanban-card';
+ li.draggable = true;
+ li.dataset.cardId = cardData.id;
+ li.dataset.colId = colId;
+
+ /* Label-Streifen */
+ const labelBar = document.createElement('div');
+ labelBar.className = 'kanban-card-label';
+ if (cardData.label && cardData.label !== 'none') {
+ labelBar.style.background = cardData.label;
+ } else {
+ labelBar.style.display = 'none';
+ }
+
+ /* Text (contenteditable) */
+ const textEl = document.createElement('div');
+ textEl.className = 'kanban-card-text';
+ textEl.contentEditable = 'true';
+ textEl.spellcheck = false;
+ textEl.textContent = cardData.text;
+ textEl.setAttribute('aria-label', 'Kartentext bearbeiten');
+
+ textEl.addEventListener('blur', () => {
+ const result = findCard(cardData.id);
+ if (result) {
+ result.card.text = textEl.textContent.trim() || result.card.text;
+ saveState();
+ }
+ });
+ // Enter β Blur statt Zeilenumbruch
+ textEl.addEventListener('keydown', e => {
+ if (e.key === 'Enter') { e.preventDefault(); textEl.blur(); }
+ });
+
+ /* LΓΆschen-Button */
+ const deleteBtn = document.createElement('button');
+ deleteBtn.type = 'button';
+ deleteBtn.className = 'kanban-card-delete';
+ deleteBtn.title = 'Karte lΓΆschen';
+ deleteBtn.innerHTML = 'β';
+ deleteBtn.addEventListener('click', () => deleteCard(cardData.id));
+
+ li.appendChild(labelBar);
+ li.appendChild(textEl);
+ li.appendChild(deleteBtn);
+
+ /* Drag Events */
+ li.addEventListener('dragstart', e => onCardDragStart(e, cardData.id, colId, li));
+ li.addEventListener('dragend', () => onCardDragEnd());
+
+ /* Rechtsklick β Label Picker */
+ li.addEventListener('contextmenu', e => {
+ e.preventDefault();
+ openLabelPicker(e, cardData.id);
+ });
+
+ return li;
+ }
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Spalten-Operationen
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function addColumn () {
+ const name = prompt('Name der neuen Spalte:');
+ if (!name || !name.trim()) return;
+ const colData = { id: uid(), title: name.trim(), cards: [] };
+ state.columns.push(colData);
+ saveState();
+ renderBoard();
+ }
+
+ function renameColumn (colId, titleEl) {
+ const col = findColumn(colId);
+ if (!col) return;
+ const oldTitle = col.title;
+ titleEl.contentEditable = 'true';
+ titleEl.focus();
+ // Cursor ans Ende
+ const range = document.createRange();
+ range.selectNodeContents(titleEl);
+ range.collapse(false);
+ window.getSelection().removeAllRanges();
+ window.getSelection().addRange(range);
+
+ function finish () {
+ titleEl.contentEditable = 'false';
+ col.title = titleEl.textContent.trim() || oldTitle;
+ titleEl.textContent = col.title;
+ saveState();
+ }
+ titleEl.addEventListener('blur', finish, { once: true });
+ titleEl.addEventListener('keydown', e => {
+ if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
+ if (e.key === 'Escape') { titleEl.textContent = oldTitle; titleEl.blur(); }
+ }, { once: true });
+ }
+
+ function deleteColumn (colId) {
+ const col = findColumn(colId);
+ if (!col) return;
+ const count = col.cards.length;
+ const msg = count > 0
+ ? `Spalte "${col.title}" mit ${count} Karte(n) lΓΆschen?`
+ : `Spalte "${col.title}" lΓΆschen?`;
+ if (!confirm(msg)) return;
+ state.columns = state.columns.filter(c => c.id !== colId);
+ saveState();
+ renderBoard();
+ }
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Karten-Operationen
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function addCard (colId, cardList, badge, colData) {
+ const cardData = { id: uid(), text: 'Neue Karteβ¦', label: 'none' };
+ colData.cards.push(cardData);
+ saveState();
+
+ const li = buildCard(cardData, colId);
+ // Vor dem Placeholder (falls vorhanden), sonst ans Ende
+ const ph = cardList.querySelector('[data-placeholder]');
+ if (ph) cardList.insertBefore(li, ph);
+ else cardList.appendChild(li);
+
+ badge.textContent = colData.cards.length;
+
+ // Direkt zum Bearbeiten fokussieren
+ const textEl = li.querySelector('.kanban-card-text');
+ textEl.focus();
+ // Alles selektieren
+ document.execCommand('selectAll', false, null);
+ }
+
+ function deleteCard (cardId) {
+ for (const col of state.columns) {
+ const idx = col.cards.findIndex(c => c.id === cardId);
+ if (idx !== -1) {
+ col.cards.splice(idx, 1);
+ saveState();
+ renderBoard();
+ return;
+ }
+ }
+ }
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Drag & Drop
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ function onCardDragStart (e, cardId, colId, li) {
+ dragCardId = cardId;
+ dragColumnId = colId;
+ li.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', cardId); // Firefox fix
+ }
+
+ function onCardDragEnd () {
+ dragCardId = null;
+ dragColumnId = null;
+ // Alle Highlights entfernen
+ document.querySelectorAll('.kanban-column.drag-over').forEach(c => c.classList.remove('drag-over'));
+ document.querySelectorAll('.kanban-card.dragging').forEach(c => c.classList.remove('dragging'));
+ document.querySelectorAll('.kanban-drop-placeholder').forEach(p => p.remove());
+ }
+
+ function onColumnDragEnter (e, colEl) {
+ e.preventDefault();
+ colEl.classList.add('drag-over');
+ }
+
+ function onColumnDragLeave (e, colEl) {
+ // Nur entfernen, wenn wirklich die Spalte verlassen wird (nicht ein Kind)
+ if (!colEl.contains(e.relatedTarget)) {
+ colEl.classList.remove('drag-over');
+ }
+ }
+
+ /**
+ * Bestimmt, vor welchem Element die Karte eingesetzt werden soll.
+ * Gibt null zurΓΌck β ans Ende der Liste.
+ */
+ function getDragAfterElement (cardList, clientY) {
+ const draggableEls = [...cardList.querySelectorAll('.kanban-card:not(.dragging)')];
+ let closest = { offset: Number.NEGATIVE_INFINITY, element: null };
+ for (const el of draggableEls) {
+ const box = el.getBoundingClientRect();
+ const offset = clientY - box.top - box.height / 2;
+ if (offset < 0 && offset > closest.offset) {
+ closest = { offset, element: el };
+ }
+ }
+ return closest.element;
+ }
+
+ function onCardListDragOver (e, cardList) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+
+ // Placeholder bewegen
+ let ph = cardList.querySelector('.kanban-drop-placeholder');
+ if (!ph) {
+ ph = document.createElement('li');
+ ph.className = 'kanban-drop-placeholder';
+ ph.dataset.placeholder = 'true';
+ }
+ const after = getDragAfterElement(cardList, e.clientY);
+ if (after) cardList.insertBefore(ph, after);
+ else cardList.appendChild(ph);
+ }
+
+ function onCardListDrop (e, targetColId, cardList) {
+ e.preventDefault();
+ if (!dragCardId) return;
+
+ const targetCol = findColumn(targetColId);
+ const result = findCard(dragCardId);
+ if (!result || !targetCol) return;
+
+ const { card, column: sourceCol } = result;
+
+ // Aus Quell-Spalte entfernen
+ sourceCol.cards = sourceCol.cards.filter(c => c.id !== dragCardId);
+
+ // EinfΓΌgeposition bestimmen (anhand Placeholder)
+ const ph = cardList.querySelector('.kanban-drop-placeholder');
+ let insertIndex = targetCol.cards.length; // Default: ans Ende
+
+ if (ph) {
+ // Placeholder-Position im DOM β entsprechende Index im State
+ const siblings = [...cardList.querySelectorAll('.kanban-card')];
+ const phIndex = [...cardList.children].indexOf(ph);
+ // Karten vor dem Placeholder zΓ€hlen
+ let count = 0;
+ for (const sib of cardList.children) {
+ if (sib === ph) break;
+ if (sib.classList.contains('kanban-card')) count++;
+ }
+ insertIndex = count;
+ }
+
+ targetCol.cards.splice(insertIndex, 0, card);
+ saveState();
+ renderBoard();
+ }
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Label Picker
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const labelPicker = document.getElementById('label-picker');
+
+ function openLabelPicker (e, cardId) {
+ labelTargetCardId = cardId;
+ labelPicker.removeAttribute('hidden');
+
+ // Positionieren
+ const x = Math.min(e.clientX, window.innerWidth - labelPicker.offsetWidth - 12);
+ const y = Math.min(e.clientY, window.innerHeight - labelPicker.offsetHeight - 12);
+ labelPicker.style.left = (x + window.scrollX) + 'px';
+ labelPicker.style.top = (y + window.scrollY) + 'px';
+ }
+
+ function closeLabelPicker () {
+ labelPicker.setAttribute('hidden', '');
+ labelTargetCardId = null;
+ }
+
+ labelPicker.querySelectorAll('.label-swatch').forEach(swatch => {
+ swatch.addEventListener('click', () => {
+ if (!labelTargetCardId) return;
+ const result = findCard(labelTargetCardId);
+ if (!result) return;
+ result.card.label = swatch.dataset.color;
+ saveState();
+ renderBoard();
+ closeLabelPicker();
+ });
+ });
+
+ document.addEventListener('click', e => {
+ if (!labelPicker.hasAttribute('hidden') && !labelPicker.contains(e.target)) {
+ closeLabelPicker();
+ }
+ });
+
+ document.addEventListener('keydown', e => {
+ if (e.key === 'Escape') closeLabelPicker();
+ });
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Toolbar-Aktionen
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ /* + Spalte */
+ document.getElementById('btn-add-column').addEventListener('click', addColumn);
+
+ /* Export JSON */
+ document.getElementById('btn-export').addEventListener('click', () => {
+ const json = JSON.stringify(state, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'kanban-board-' + new Date().toISOString().slice(0, 10) + '.json';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(url);
+ });
+
+ /* Reset */
+ document.getElementById('btn-reset').addEventListener('click', () => {
+ if (!confirm('Board komplett zurΓΌcksetzen? Alle Daten gehen verloren!')) return;
+ localStorage.removeItem(STORAGE_KEY);
+ state = JSON.parse(JSON.stringify(DEFAULT_BOARD));
+ // Neue IDs erzeugen damit UIDs nicht doppelt sind
+ state.columns.forEach(col => {
+ col.id = uid();
+ col.cards.forEach(card => { card.id = uid(); });
+ });
+ saveState();
+ renderBoard();
+ });
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Init
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ renderBoard();
+
+})();
diff --git a/kanban.html b/kanban.html
new file mode 100644
index 0000000..422dc91
--- /dev/null
+++ b/kanban.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+ Kanban-Board β David & Karo
+
+
+
+
+
+
+
+
+
+
+
+
π·οΈ Label-Farbe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kontakt.html b/kontakt.html
index 9e8afea..167d237 100644
--- a/kontakt.html
+++ b/kontakt.html
@@ -128,6 +128,7 @@
diff --git a/notenrechner.html b/notenrechner.html
index 75c16ee..549859e 100644
--- a/notenrechner.html
+++ b/notenrechner.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
diff --git a/team.html b/team.html
index b2cf778..626e78c 100644
--- a/team.html
+++ b/team.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
diff --git a/textanalyse.html b/textanalyse.html
index 69f14ff..87256f5 100644
--- a/textanalyse.html
+++ b/textanalyse.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
diff --git a/ueber_uns.html b/ueber_uns.html
index bce70bc..bab2c90 100644
--- a/ueber_uns.html
+++ b/ueber_uns.html
@@ -35,6 +35,7 @@
Notenrechner
Textanalyse
API Demo
+ Kanban
Kontakt
Impressum
@@ -110,6 +111,7 @@