diff --git a/components/navbar.html b/components/navbar.html index b1dcf5d..f8b4891 100644 --- a/components/navbar.html +++ b/components/navbar.html @@ -15,6 +15,7 @@
  • Notenrechner
  • Textanalyse
  • Notenrechner (DOM)
  • +
  • Kanban Board
  • PDF ↓
  • diff --git a/css/kanban.css b/css/kanban.css new file mode 100644 index 0000000..4bde3ea --- /dev/null +++ b/css/kanban.css @@ -0,0 +1,417 @@ +/* ====================== KANBAN BOARD STYLES ====================== */ + +.kanban-main { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2rem 0; +} + +.kanban-header { + max-width: 1400px; + margin: 0 auto 2rem; + padding: 0 2rem; + text-align: center; +} + +.kanban-header h1 { + font-size: 3rem; + color: white; + margin: 0 0 0.5rem 0; + text-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.kanban-header p { + color: rgba(255, 255, 255, 0.9); + font-size: 1.1rem; + margin-bottom: 2rem; +} + +.header-actions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.btn { + padding: 0.8rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: capitalize; +} + +.btn-primary { + background: white; + color: #667eea; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 2px solid rgba(255, 255, 255, 0.5); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.3); + border-color: white; +} + +.btn-danger { + background: rgba(239, 68, 68, 0.2); + color: #fecaca; + border: 2px solid rgba(239, 68, 68, 0.5); +} + +.btn-danger:hover { + background: rgba(239, 68, 68, 0.3); + border-color: #fecaca; +} + +/* ====================== KANBAN CONTAINER ====================== */ + +.kanban-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; + display: flex; + gap: 2rem; + overflow-x: auto; + padding-bottom: 2rem; +} + +.kanban-container::-webkit-scrollbar { + height: 8px; +} + +.kanban-container::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); +} + +.kanban-container::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; +} + +.kanban-container::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); +} + +/* ====================== SPALTEN ====================== */ + +.kanban-column { + flex: 0 0 320px; + background: white; + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + max-height: 80vh; + overflow: hidden; + transition: all 0.3s ease; +} + +.kanban-column.drag-over { + background: #f0f4ff; + box-shadow: 0 12px 35px rgba(102, 126, 234, 0.3); + transform: scale(1.02); +} + +.kanban-column-header { + padding: 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 12px 12px 0 0; +} + +.kanban-column-title { + font-size: 1.2rem; + font-weight: 700; + margin: 0; +} + +.column-delete-btn { + background: rgba(255, 255, 255, 0.2); + border: none; + color: white; + width: 32px; + height: 32px; + border-radius: 6px; + cursor: pointer; + font-size: 1.2rem; + transition: all 0.2s ease; +} + +.column-delete-btn:hover { + background: rgba(255, 255, 255, 0.4); +} + +.kanban-cards { + flex: 1; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; +} + +.kanban-cards::-webkit-scrollbar { + width: 6px; +} + +.kanban-cards::-webkit-scrollbar-track { + background: transparent; +} + +.kanban-cards::-webkit-scrollbar-thumb { + background: #e2e8f0; + border-radius: 3px; +} + +.kanban-cards::-webkit-scrollbar-thumb:hover { + background: #cbd5e1; +} + +/* ====================== KARTEN ====================== */ + +.kanban-card { + background: white; + border: 2px solid #e2e8f0; + border-radius: 8px; + padding: 1rem; + cursor: move; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + position: relative; + min-height: 60px; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.5rem; +} + +.kanban-card:hover { + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); + border-color: #667eea; + transform: translateY(-2px); +} + +.kanban-card.dragging { + opacity: 0.5; + transform: rotate(5deg); +} + +.kanban-card.drag-over-card { + border-top: 3px solid #667eea; + margin-top: 8px; +} + +.card-content { + flex: 1; + word-break: break-word; +} + +.card-text { + margin: 0; + color: #1e293b; + font-size: 0.95rem; + line-height: 1.5; + outline: none; + min-height: 1.5em; +} + +.card-text:focus { + background: #f0f4ff; + padding: 0.25rem; + border-radius: 4px; +} + +.card-label { + display: inline-block; + padding: 0.3rem 0.6rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + color: white; + margin-top: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.card-label:hover { + transform: scale(1.1); +} + +.label-red { + background: #ef4444; +} + +.label-blue { + background: #3b82f6; +} + +.label-yellow { + background: #eab308; +} + +.label-green { + background: #22c55e; +} + +.label-purple { + background: #a855f7; +} + +.card-actions { + display: flex; + gap: 0.3rem; + flex-direction: column; +} + +.card-delete-btn { + background: #ef4444; + border: none; + color: white; + width: 28px; + height: 28px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.card-delete-btn:hover { + background: #dc2626; + transform: scale(1.15); +} + +/* ====================== ADD BUTTON ====================== */ + +.add-card-btn { + width: 100%; + padding: 0.8rem; + background: #f1f5f9; + border: 2px dashed #cbd5e1; + border-radius: 8px; + color: #64748b; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-top: auto; +} + +.add-card-btn:hover { + background: #e2e8f0; + border-color: #667eea; + color: #667eea; +} + +/* ====================== COLOR PICKER ====================== */ + +.color-picker { + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); + z-index: 1000; + overflow: hidden; +} + +.color-picker-title { + padding: 0.8rem 1rem; + background: #f1f5f9; + font-size: 0.9rem; + font-weight: 600; + color: #475569; + border-bottom: 1px solid #e2e8f0; +} + +.color-options { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + padding: 0.8rem; +} + +.color-option { + width: 40px; + height: 40px; + border-radius: 6px; + cursor: pointer; + border: 3px solid transparent; + transition: all 0.2s ease; +} + +.color-option:hover { + transform: scale(1.1); +} + +.color-option.active { + border-color: #1e293b; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +} + +/* ====================== RESPONSIVE ====================== */ + +@media (max-width: 768px) { + .kanban-header h1 { + font-size: 2rem; + } + + .kanban-container { + padding: 0 1rem; + gap: 1rem; + } + + .kanban-column { + flex: 0 0 280px; + } + + .header-actions { + gap: 0.5rem; + } + + .btn { + padding: 0.6rem 1rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .kanban-header h1 { + font-size: 1.5rem; + } + + .kanban-container { + padding: 0 0.5rem; + gap: 0.5rem; + } + + .kanban-column { + flex: 0 0 250px; + } + + .header-actions { + flex-direction: column; + } + + .btn { + width: 100%; + } +} diff --git a/js/kanban.js b/js/kanban.js new file mode 100644 index 0000000..1d68dba --- /dev/null +++ b/js/kanban.js @@ -0,0 +1,475 @@ +// ====================== KANBAN BOARD APP ====================== +// Single-Page Kanban Board mit Drag & Drop und localStorage Persistenz + +class KanbanBoard { + constructor() { + this.container = document.getElementById('kanbanContainer'); + this.board = this.loadFromLocalStorage() || this.getDefaultBoard(); + this.draggedCard = null; + this.draggedFromColumn = null; + this.colorPickerCard = null; + + this.initializeEventListeners(); + this.render(); + } + + /** + * Standard-Board mit 3 Spalten + */ + getDefaultBoard() { + return { + columns: [ + { + id: this.generateId(), + title: 'To Do', + cards: [ + { id: this.generateId(), text: 'Beispiel Aufgabe 1', color: null }, + { id: this.generateId(), text: 'Beispiel Aufgabe 2', color: null } + ] + }, + { + id: this.generateId(), + title: 'In Arbeit', + cards: [ + { id: this.generateId(), text: 'Aktuelle Aufgabe', color: null } + ] + }, + { + id: this.generateId(), + title: 'Erledigt', + cards: [ + { id: this.generateId(), text: 'Abgeschlossene Aufgabe', color: null } + ] + } + ] + }; + } + + /** + * Event Listener initialisieren + */ + initializeEventListeners() { + document.getElementById('addColumnBtn').addEventListener('click', () => this.addColumn()); + document.getElementById('exportBtn').addEventListener('click', () => this.exportAsJSON()); + document.getElementById('importBtn').addEventListener('click', () => this.triggerImport()); + document.getElementById('resetBtn').addEventListener('click', () => this.resetBoard()); + document.getElementById('importFileInput').addEventListener('change', (e) => this.importFromJSON(e)); + + // Close color picker wenn außerhalb geklickt wird + document.addEventListener('click', (e) => { + if (!e.target.closest('.color-picker') && !e.target.closest('.kanban-card')) { + this.hideColorPicker(); + } + }); + } + + /** + * Spalten und Karten rendern + */ + render() { + this.container.innerHTML = ''; + + this.board.columns.forEach(column => { + const columnEl = document.createElement('div'); + columnEl.className = 'kanban-column'; + columnEl.id = `column-${column.id}`; + columnEl.draggable = false; + + // Column Header + const headerEl = document.createElement('div'); + headerEl.className = 'kanban-column-header'; + + const titleEl = document.createElement('h2'); + titleEl.className = 'kanban-column-title'; + titleEl.textContent = `${column.title} (${column.cards.length})`; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'column-delete-btn'; + deleteBtn.textContent = '✕'; + deleteBtn.addEventListener('click', () => this.deleteColumn(column.id)); + + headerEl.appendChild(titleEl); + headerEl.appendChild(deleteBtn); + + // Cards Container + const cardsEl = document.createElement('div'); + cardsEl.className = 'kanban-cards'; + cardsEl.id = `cards-${column.id}`; + + // Drag & Drop Events für Spalte + cardsEl.addEventListener('dragover', (e) => this.handleDragOver(e, column.id)); + cardsEl.addEventListener('dragleave', (e) => this.handleDragLeave(e, column.id)); + cardsEl.addEventListener('drop', (e) => this.handleDrop(e, column.id)); + cardsEl.addEventListener('dragenter', (e) => this.handleDragEnter(e, column.id)); + + // Karten rendern + column.cards.forEach(card => { + const cardEl = this.createCardElement(card, column.id); + cardsEl.appendChild(cardEl); + }); + + // Add Card Button + const addCardBtn = document.createElement('button'); + addCardBtn.className = 'add-card-btn'; + addCardBtn.textContent = '+ Neue Karte'; + addCardBtn.addEventListener('click', () => this.addCard(column.id)); + + cardsEl.appendChild(addCardBtn); + columnEl.appendChild(headerEl); + columnEl.appendChild(cardsEl); + this.container.appendChild(columnEl); + }); + + this.save(); + } + + /** + * Karten-Element erstellen + */ + createCardElement(card, columnId) { + const cardEl = document.createElement('div'); + cardEl.className = 'kanban-card'; + cardEl.id = `card-${card.id}`; + cardEl.draggable = true; + + // Drag Events + cardEl.addEventListener('dragstart', (e) => this.handleDragStart(e, card, columnId)); + cardEl.addEventListener('dragend', (e) => this.handleDragEnd(e)); + + // Card Content + const contentEl = document.createElement('div'); + contentEl.className = 'card-content'; + + const textEl = document.createElement('p'); + textEl.className = 'card-text'; + textEl.textContent = card.text; + textEl.contentEditable = 'true'; + textEl.addEventListener('blur', () => { + card.text = textEl.textContent; + this.save(); + }); + + contentEl.appendChild(textEl); + + // Label anzeigen, falls vorhanden + if (card.color) { + const labelEl = document.createElement('div'); + labelEl.className = `card-label label-${card.color}`; + labelEl.textContent = this.getColorName(card.color); + contentEl.appendChild(labelEl); + } + + // Rechtsklick für Farben (Bonus) + cardEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + this.showColorPicker(e, card, columnId); + }); + + // Card Actions + const actionsEl = document.createElement('div'); + actionsEl.className = 'card-actions'; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'card-delete-btn'; + deleteBtn.textContent = '✕'; + deleteBtn.addEventListener('click', () => this.deleteCard(columnId, card.id)); + + actionsEl.appendChild(deleteBtn); + + cardEl.appendChild(contentEl); + cardEl.appendChild(actionsEl); + + return cardEl; + } + + /** + * Spalte hinzufügen + */ + addColumn() { + const title = prompt('Spaltenname eingeben:'); + if (!title || title.trim() === '') return; + + const newColumn = { + id: this.generateId(), + title: title.trim(), + cards: [] + }; + + this.board.columns.push(newColumn); + this.render(); + } + + /** + * Spalte löschen + */ + deleteColumn(columnId) { + if (confirm('Spalte und alle Karten löschen?')) { + this.board.columns = this.board.columns.filter(col => col.id !== columnId); + this.render(); + } + } + + /** + * Karte hinzufügen + */ + addCard(columnId) { + const column = this.board.columns.find(col => col.id === columnId); + if (!column) return; + + const newCard = { + id: this.generateId(), + text: 'Neue Aufgabe...', + color: null + }; + + column.cards.push(newCard); + this.render(); + } + + /** + * Karte löschen + */ + deleteCard(columnId, cardId) { + const column = this.board.columns.find(col => col.id === columnId); + if (!column) return; + + column.cards = column.cards.filter(card => card.id !== cardId); + this.render(); + } + + /** + * ====================== DRAG & DROP ====================== + */ + + handleDragStart(e, card, columnId) { + this.draggedCard = card; + this.draggedFromColumn = columnId; + const cardEl = document.getElementById(`card-${card.id}`); + cardEl.classList.add('dragging'); + } + + handleDragEnd(e) { + document.querySelectorAll('.kanban-card').forEach(card => { + card.classList.remove('dragging'); + card.classList.remove('drag-over-card'); + }); + document.querySelectorAll('.kanban-column').forEach(col => { + col.classList.remove('drag-over'); + }); + } + + handleDragOver(e, columnId) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const cardsContainer = document.getElementById(`cards-${columnId}`); + const afterElement = this.getDragAfterElement(cardsContainer, e.clientY); + + if (afterElement == null) { + // Vor den Add Button einfügen + cardsContainer.insertBefore(this.draggedCard, cardsContainer.lastElementChild); + } else { + cardsContainer.insertBefore(this.draggedCard, afterElement); + } + } + + handleDragEnter(e, columnId) { + const columnEl = document.getElementById(`column-${columnId}`); + columnEl.classList.add('drag-over'); + } + + handleDragLeave(e, columnId) { + const columnEl = document.getElementById(`column-${columnId}`); + const cardsEl = document.getElementById(`cards-${columnId}`); + + // Nur entfernen wenn wirklich die Spalte verlassen wird + if (e.target === cardsEl || e.target.classList.contains('kanban-cards')) { + columnEl.classList.remove('drag-over'); + } + } + + handleDrop(e, columnId) { + e.preventDefault(); + + const columnEl = document.getElementById(`column-${columnId}`); + columnEl.classList.remove('drag-over'); + + if (!this.draggedCard) return; + + // Alte Spalte aktualisieren + const oldColumn = this.board.columns.find(col => col.id === this.draggedFromColumn); + if (oldColumn) { + oldColumn.cards = oldColumn.cards.filter(card => card.id !== this.draggedCard.id); + } + + // Neue Spalte aktualisieren + const newColumn = this.board.columns.find(col => col.id === columnId); + if (newColumn) { + // Prüfen ob die Karte bereits in der neuen Spalte ist (aus derselben Spalte kommend) + if (!newColumn.cards.find(card => card.id === this.draggedCard.id)) { + newColumn.cards.push(this.draggedCard); + } + } + + this.draggedCard = null; + this.draggedFromColumn = null; + this.render(); + } + + /** + * Hilfsfunktion um Position des draggable Elements zu bestimmen + */ + getDragAfterElement(container, y) { + const draggableElements = [...container.querySelectorAll('.kanban-card:not(.dragging)')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + + /** + * ====================== FARBEN (BONUS) ====================== + */ + + showColorPicker(e, card, columnId) { + this.hideColorPicker(); + + const picker = document.createElement('div'); + picker.className = 'color-picker'; + picker.style.left = e.clientX + 'px'; + picker.style.top = e.clientY + 'px'; + picker.id = 'colorPickerMenu'; + + const title = document.createElement('div'); + title.className = 'color-picker-title'; + title.textContent = 'Farbe wählen:'; + + const options = document.createElement('div'); + options.className = 'color-options'; + + const colors = ['red', 'blue', 'yellow', 'green', 'purple']; + colors.forEach(color => { + const option = document.createElement('div'); + option.className = `color-option label-${color}`; + if (card.color === color) option.classList.add('active'); + + option.addEventListener('click', () => { + card.color = card.color === color ? null : color; + const column = this.board.columns.find(col => col.id === columnId); + const cardToUpdate = column.cards.find(c => c.id === card.id); + if (cardToUpdate) cardToUpdate.color = card.color; + this.save(); + this.render(); + this.hideColorPicker(); + }); + + options.appendChild(option); + }); + + picker.appendChild(title); + picker.appendChild(options); + document.body.appendChild(picker); + } + + hideColorPicker() { + const picker = document.getElementById('colorPickerMenu'); + if (picker) picker.remove(); + } + + getColorName(color) { + const names = { + 'red': '🔴', + 'blue': '🔵', + 'yellow': '🟡', + 'green': '🟢', + 'purple': '🟣' + }; + return names[color] || ''; + } + + /** + * ====================== PERSISTENZ ====================== + */ + + save() { + localStorage.setItem('kanbanBoard', JSON.stringify(this.board)); + } + + loadFromLocalStorage() { + const stored = localStorage.getItem('kanbanBoard'); + return stored ? JSON.parse(stored) : null; + } + + /** + * ====================== EXPORT/IMPORT ====================== + */ + + exportAsJSON() { + const dataStr = JSON.stringify(this.board, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = `kanban-board-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + triggerImport() { + document.getElementById('importFileInput').click(); + } + + importFromJSON(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const imported = JSON.parse(event.target.result); + if (imported.columns && Array.isArray(imported.columns)) { + this.board = imported; + this.save(); + this.render(); + alert('Board erfolgreich importiert!'); + } else { + alert('Ungültiges JSON-Format!'); + } + } catch (error) { + alert('Fehler beim Import: ' + error.message); + } + }; + reader.readAsText(file); + } + + resetBoard() { + if (confirm('Möchtest du das Board wirklich zurücksetzen?')) { + this.board = this.getDefaultBoard(); + this.save(); + this.render(); + } + } + + /** + * Utility: Eindeutige ID generieren + */ + generateId() { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } +} + +// ====================== APP STARTEN ====================== + +document.addEventListener('DOMContentLoaded', () => { + new KanbanBoard(); +}); diff --git a/kanban.html b/kanban.html new file mode 100644 index 0000000..04f02f2 --- /dev/null +++ b/kanban.html @@ -0,0 +1,51 @@ + + + + + + + Kanban Board - EIS Projekt + + + + + + + +
    +
    +

    📊 Kanban Board

    +

    Organisiere deine Aufgaben mit Drag & Drop

    + +
    + + + + +
    +
    + +
    + +
    + + + +
    + + + + + + +