Mega-Bonus: Kanban-Board – Drag & Drop, Labels, localStorage, JSON-Export

This commit is contained in:
David Kertzscher 2026-05-20 13:10:02 +00:00
parent d2b2132605
commit dd7c36a14d
13 changed files with 998 additions and 1 deletions

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html" class="active">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -132,6 +133,7 @@ fetch('https://jsonplaceholder.typicode.com/users')
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

View file

@ -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; }
}

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -177,6 +178,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -89,6 +90,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html" class="active">Impressum</a>
</nav>
@ -119,6 +120,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -89,6 +90,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

503
js/kanban.js Normal file
View file

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

88
kanban.html Normal file
View file

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Kanban-Board Drag & Drop David & Karo EIS Projekt">
<title>Kanban-Board David & Karo</title>
<link rel="stylesheet" href="css/style.css">
<link rel="icon" href="img/logo.png" type="image/png">
</head>
<body>
<header>
<div class="header-container">
<div class="logo">
<img src="img/logo.png" alt="Logo">
<div class="logo-text">
<span class="institution">David & Karo</span>
<span class="project">EIS Projekt</span>
</div>
</div>
<button id="dark-mode-toggle" type="button" class="dark-mode-toggle" title="Dark Mode aktivieren">
<span class="dark-mode-icon">🌙</span>
</button>
<button id="nav-toggle" type="button" class="nav-toggle" aria-label="Menü öffnen" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<nav id="main-nav">
<a href="index.html">Start</a>
<a href="ueber_uns.html">Über uns</a>
<a href="eis_projekt.html">Projekt</a>
<a href="team.html">Team</a>
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html" class="active">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
</div>
</header>
<main class="kanban-main">
<div class="kanban-header">
<div class="kanban-title">
<h1>📋 Kanban-Board</h1>
<p>Drag &amp; Drop · localStorage · Labels</p>
</div>
<div class="kanban-toolbar">
<button type="button" id="btn-add-column" class="btn btn-kanban"> Spalte</button>
<button type="button" id="btn-export" class="btn btn-kanban btn-secondary">⬇️ Export JSON</button>
<button type="button" id="btn-reset" class="btn btn-kanban btn-danger">🗑️ Reset</button>
</div>
</div>
<!-- Farb-Label Picker (Kontext-Menü) -->
<div id="label-picker" class="label-picker" hidden role="menu" aria-label="Label-Farbe wählen">
<p class="label-picker-title">🏷️ Label-Farbe</p>
<div class="label-colors">
<button type="button" class="label-swatch" data-color="none" style="background:#e2e8f0" title="Kein Label"></button>
<button type="button" class="label-swatch" data-color="#f87171" style="background:#f87171" title="Rot"></button>
<button type="button" class="label-swatch" data-color="#fb923c" style="background:#fb923c" title="Orange"></button>
<button type="button" class="label-swatch" data-color="#facc15" style="background:#facc15" title="Gelb"></button>
<button type="button" class="label-swatch" data-color="#4ade80" style="background:#4ade80" title="Grün"></button>
<button type="button" class="label-swatch" data-color="#60a5fa" style="background:#60a5fa" title="Blau"></button>
<button type="button" class="label-swatch" data-color="#a78bfa" style="background:#a78bfa" title="Lila"></button>
<button type="button" class="label-swatch" data-color="#f472b6" style="background:#f472b6" title="Pink"></button>
<button type="button" class="label-swatch" data-color="#2dd4bf" style="background:#2dd4bf" title="Türkis"></button>
</div>
</div>
<!-- Das Board selbst -->
<div id="kanban-board" class="kanban-board"></div>
</main>
<footer>
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>
<script src="js/script.js"></script>
<script src="js/kanban.js"></script>
</body>
</html>

View file

@ -128,6 +128,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html" class="active">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html" class="active">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -35,6 +35,7 @@
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -110,6 +111,7 @@
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>