From dd7c36a14dedb2be015feb8b037b19d49a3a06f4 Mon Sep 17 00:00:00 2001 From: David Kertzscher Date: Wed, 20 May 2026 13:10:02 +0000 Subject: [PATCH] =?UTF-8?q?Mega-Bonus:=20Kanban-Board=20=E2=80=93=20Drag?= =?UTF-8?q?=20&=20Drop,=20Labels,=20localStorage,=20JSON-Export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.html | 4 +- css/style.css | 390 +++++++++++++++++++++++++++++++++++ eis_projekt.html | 2 + galerie.html | 2 + impressum.html | 2 + index.html | 2 + js/kanban.js | 503 ++++++++++++++++++++++++++++++++++++++++++++++ kanban.html | 88 ++++++++ kontakt.html | 1 + notenrechner.html | 1 + team.html | 1 + textanalyse.html | 1 + ueber_uns.html | 2 + 13 files changed, 998 insertions(+), 1 deletion(-) create mode 100644 js/kanban.js create mode 100644 kanban.html 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 + + + + +
+
+ + + + +
+
+ +
+
+
+

πŸ“‹ Kanban-Board

+

Drag & Drop Β· localStorage Β· Labels

+
+
+ + + +
+
+ + + + + +
+
+ + + + + + + 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 @@