Mega-Bonus: Kanban-Board – Drag & Drop, Labels, localStorage, JSON-Export
This commit is contained in:
parent
d2b2132605
commit
dd7c36a14d
13 changed files with 998 additions and 1 deletions
4
api.html
4
api.html
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html" class="active">API Demo</a>
|
<a href="api.html" class="active">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -132,7 +133,8 @@ fetch('https://jsonplaceholder.typicode.com/users')
|
||||||
<p>© 2026 – Made with 💕 & ✨ – David & Karo</p>
|
<p>© 2026 – Made with 💕 & ✨ – David & Karo</p>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kanban.html">Kanban</a>
|
||||||
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
390
css/style.css
390
css/style.css
|
|
@ -3187,3 +3187,393 @@ body.dark .api-user-details a { color: #f9a8d4; }
|
||||||
#api-search { flex: unset; width: 100%; }
|
#api-search { flex: unset; width: 100%; }
|
||||||
.user-list { grid-template-columns: 1fr; }
|
.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; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -177,6 +178,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -89,6 +90,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html" class="active">Impressum</a>
|
<a href="impressum.html" class="active">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -119,6 +120,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -89,6 +90,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
503
js/kanban.js
Normal file
503
js/kanban.js
Normal 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
88
kanban.html
Normal 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 & 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>© 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>
|
||||||
|
|
@ -128,6 +128,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html" class="active">Notenrechner</a>
|
<a href="notenrechner.html" class="active">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html" class="active">Textanalyse</a>
|
<a href="textanalyse.html" class="active">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<a href="notenrechner.html">Notenrechner</a>
|
<a href="notenrechner.html">Notenrechner</a>
|
||||||
<a href="textanalyse.html">Textanalyse</a>
|
<a href="textanalyse.html">Textanalyse</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -110,6 +111,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="impressum.html">Impressum</a>
|
<a href="impressum.html">Impressum</a>
|
||||||
<a href="api.html">API Demo</a>
|
<a href="api.html">API Demo</a>
|
||||||
|
<a href="kanban.html">Kanban</a>
|
||||||
<a href="kontakt.html">Kontakt</a>
|
<a href="kontakt.html">Kontakt</a>
|
||||||
<a href="#">Datenschutz</a>
|
<a href="#">Datenschutz</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue