Skip to content

Instantly share code, notes, and snippets.

@netologist
Created April 12, 2025 13:30
Show Gist options
  • Save netologist/d5dc0f916b52701ac9b8089f65df0c4b to your computer and use it in GitHub Desktop.
Save netologist/d5dc0f916b52701ac9b8089f65df0c4b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Swimlane Destekli Kanban Board</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
gap: 10px;
flex-wrap: wrap;
}
.form-group {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
input, select, button, textarea {
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.kanban-board {
display: flex;
overflow-x: auto;
padding-bottom: 10px;
gap: 20px;
}
.column {
flex: 0 0 300px;
background-color: #ebecf0;
border-radius: 5px;
padding: 10px;
}
.column-header {
font-weight: bold;
padding: 10px;
background-color: #dfe1e6;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.column-title {
font-size: 16px;
}
.task-count {
background-color: rgba(0, 0, 0, 0.1);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
.swimlane {
margin-bottom: 20px;
border-bottom: 2px dashed #ccc;
padding-bottom: 10px;
}
.swimlane:last-child {
border-bottom: none;
}
.swimlane-title {
font-weight: bold;
padding: 5px;
background-color: #e3e9f2;
border-radius: 3px;
margin-bottom: 10px;
font-size: 14px;
}
.card-list {
min-height: 50px;
transition: min-height 0.3s ease;
}
.card {
background-color: white;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
cursor: grab;
position: relative;
}
.card:hover {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.card-title {
font-weight: bold;
margin-bottom: 5px;
}
.card-description {
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.card-metadata {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #999;
}
.card-swimlane-indicator {
position: absolute;
top: 5px;
right: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
}
.delete-card {
position: absolute;
top: 5px;
right: 5px;
color: #ff5252;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
visibility: hidden;
padding: 0;
}
.card:hover .delete-card {
visibility: visible;
}
.card.dragging {
opacity: 0.5;
}
.swimlane-colors {
display: flex;
gap: 10px;
margin-top: 10px;
}
.color-swatch {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
}
.color-swatch.selected {
border-color: #333;
}
/* Placeholder için stil */
.card-placeholder {
background-color: rgba(0, 0, 0, 0.05);
border: 1px dashed #ccc;
border-radius: 5px;
height: 80px;
margin-bottom: 10px;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow: auto;
}
.modal-content {
background-color: #fff;
margin: 10% auto;
padding: 20px;
border-radius: 5px;
width: 60%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #333;
}
textarea {
width: 100%;
height: 200px;
font-family: monospace;
margin-bottom: 15px;
}
.export-import {
display: flex;
gap: 10px;
margin-left: auto;
}
.export-button, .import-button {
background-color: #0079bf;
}
.export-button:hover, .import-button:hover {
background-color: #026aa7;
}
</style>
</head>
<body>
<h1>Swimlane Destekli Kanban Board</h1>
<div class="controls">
<div class="form-group">
<input type="text" id="column-title" placeholder="Yeni Kolon Adı">
<button id="add-column">Kolon Ekle</button>
</div>
<div class="form-group">
<input type="text" id="swimlane-title" placeholder="Yeni Swimlane Adı">
<div class="swimlane-colors">
<div class="color-swatch selected" style="background-color: #61bd4f" data-color="#61bd4f"></div>
<div class="color-swatch" style="background-color: #f2d600" data-color="#f2d600"></div>
<div class="color-swatch" style="background-color: #ff9f1a" data-color="#ff9f1a"></div>
<div class="color-swatch" style="background-color: #eb5a46" data-color="#eb5a46"></div>
<div class="color-swatch" style="background-color: #c377e0" data-color="#c377e0"></div>
<div class="color-swatch" style="background-color: #0079bf" data-color="#0079bf"></div>
</div>
<button id="add-swimlane">Swimlane Ekle</button>
</div>
<div class="export-import">
<button id="export-button" class="export-button">Dışa Aktar</button>
<button id="import-button" class="import-button">İçe Aktar</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 20px;">
<input type="text" id="task-title" placeholder="Kart Başlığı">
<input type="text" id="task-description" placeholder="Kart Açıklaması">
<select id="task-column">
<!-- Kolonlar dinamik olarak buraya eklenecek -->
</select>
<select id="task-swimlane">
<!-- Swimlane'ler dinamik olarak buraya eklenecek -->
</select>
<button id="add-task">Kart Ekle</button>
</div>
<div class="kanban-board" id="kanban-board">
<!-- Kolonlar dinamik olarak buraya eklenecek -->
</div>
<!-- İçe Aktar Modal -->
<div id="import-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Kanban Board İçe Aktar</h2>
<span class="close">&times;</span>
</div>
<p>JSON verilerini aşağıya yapıştırın:</p>
<textarea id="import-data"></textarea>
<button id="confirm-import">İçe Aktar</button>
</div>
</div>
<!-- Dışa Aktar Modal -->
<div id="export-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Kanban Board Dışa Aktar</h2>
<span class="close">&times;</span>
</div>
<p>Verileri kopyalayın:</p>
<textarea id="export-data" readonly></textarea>
<button id="copy-export">Kopyala</button>
</div>
</div>
<script>
// Kanban Board durumu
const STORAGE_KEY = 'kanban_board_data';
let state = {
columns: [
{ id: 'col-1', title: 'Yapılacak', tasks: [] },
{ id: 'col-2', title: 'Devam Ediyor', tasks: [] },
{ id: 'col-3', title: 'Tamamlandı', tasks: [] }
],
swimlanes: [
{ id: 'swim-1', title: 'Yüksek Öncelik', color: '#eb5a46' },
{ id: 'swim-2', title: 'Normal Öncelik', color: '#f2d600' },
{ id: 'swim-3', title: 'Düşük Öncelik', color: '#61bd4f' }
],
nextTaskId: 1
};
// Local Storage'dan verileri yükle
function loadFromLocalStorage() {
const savedData = localStorage.getItem(STORAGE_KEY);
if (savedData) {
try {
state = JSON.parse(savedData);
console.log('Veriler local storage\'dan yüklendi:', state);
} catch (e) {
console.error('Local storage\'dan veri yüklenirken hata oluştu:', e);
}
}
}
// Local Storage'a verileri kaydet
function saveToLocalStorage() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
console.log('Veriler local storage\'a kaydedildi');
} catch (e) {
console.error('Local storage\'a kayıt sırasında hata oluştu:', e);
}
}
// Sayfa yüklendiğinde verileri yükle
loadFromLocalStorage();
// DOM elemanları
const kanbanBoard = document.getElementById('kanban-board');
const addColumnBtn = document.getElementById('add-column');
const addSwimlaneBtn = document.getElementById('add-swimlane');
const addTaskBtn = document.getElementById('add-task');
const columnTitleInput = document.getElementById('column-title');
const swimlaneTitleInput = document.getElementById('swimlane-title');
const taskTitleInput = document.getElementById('task-title');
const taskDescriptionInput = document.getElementById('task-description');
const taskColumnSelect = document.getElementById('task-column');
const taskSwimlaneSelect = document.getElementById('task-swimlane');
const exportBtn = document.getElementById('export-button');
const importBtn = document.getElementById('import-button');
const importModal = document.getElementById('import-modal');
const exportModal = document.getElementById('export-modal');
const importDataTextarea = document.getElementById('import-data');
const exportDataTextarea = document.getElementById('export-data');
const confirmImportBtn = document.getElementById('confirm-import');
const copyExportBtn = document.getElementById('copy-export');
// Başlangıçta board'u render et
renderBoard();
updateSelects();
// Kolon ekleme
addColumnBtn.addEventListener('click', () => {
const title = columnTitleInput.value.trim();
if (title) {
const id = 'col-' + Date.now();
state.columns.push({ id, title, tasks: [] });
saveToLocalStorage();
renderBoard();
updateSelects();
columnTitleInput.value = '';
}
});
// Swimlane ekleme
addSwimlaneBtn.addEventListener('click', () => {
const title = swimlaneTitleInput.value.trim();
if (title) {
const selectedColor = document.querySelector('.color-swatch.selected');
const color = selectedColor ? selectedColor.dataset.color : '#61bd4f';
const id = 'swim-' + Date.now();
state.swimlanes.push({ id, title, color });
saveToLocalStorage();
renderBoard();
updateSelects();
swimlaneTitleInput.value = '';
}
});
// Renk seçimi
document.querySelectorAll('.color-swatch').forEach(swatch => {
swatch.addEventListener('click', () => {
document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
swatch.classList.add('selected');
});
});
// Kart ekleme
addTaskBtn.addEventListener('click', () => {
const title = taskTitleInput.value.trim();
const description = taskDescriptionInput.value.trim();
const columnId = taskColumnSelect.value;
const swimlaneId = taskSwimlaneSelect.value;
if (title && columnId && swimlaneId) {
const task = {
id: 'task-' + state.nextTaskId++,
title,
description,
columnId,
swimlaneId,
createdAt: new Date().toLocaleString()
};
const column = state.columns.find(col => col.id === columnId);
if (column) {
column.tasks.push(task);
saveToLocalStorage();
renderBoard();
taskTitleInput.value = '';
taskDescriptionInput.value = '';
}
}
});
// Board'u render et
function renderBoard() {
kanbanBoard.innerHTML = '';
// Her swimlane için, tüm kolonlardaki maksimum kart sayısını bul
const swimlaneSizes = {};
state.swimlanes.forEach(swimlane => {
let maxCards = 0;
state.columns.forEach(column => {
const cardsInSwimlane = column.tasks.filter(task => task.swimlaneId === swimlane.id).length;
maxCards = Math.max(maxCards, cardsInSwimlane);
});
// Min 1 kart boyutu olsun (boş swimlane durumu için)
swimlaneSizes[swimlane.id] = Math.max(1, maxCards);
});
state.columns.forEach(column => {
const columnEl = document.createElement('div');
columnEl.className = 'column';
columnEl.dataset.columnId = column.id;
const columnHeader = document.createElement('div');
columnHeader.className = 'column-header';
const columnTitle = document.createElement('div');
columnTitle.className = 'column-title';
columnTitle.textContent = column.title;
const taskCount = document.createElement('div');
taskCount.className = 'task-count';
taskCount.textContent = column.tasks.length;
columnHeader.appendChild(columnTitle);
columnHeader.appendChild(taskCount);
columnEl.appendChild(columnHeader);
// Her bir swimlane için ayrı bir bölüm oluştur
state.swimlanes.forEach(swimlane => {
const swimlaneEl = document.createElement('div');
swimlaneEl.className = 'swimlane';
swimlaneEl.dataset.swimlaneId = swimlane.id;
const swimlaneTitle = document.createElement('div');
swimlaneTitle.className = 'swimlane-title';
swimlaneTitle.textContent = swimlane.title;
swimlaneTitle.style.borderLeft = `3px solid ${swimlane.color}`;
swimlaneEl.appendChild(swimlaneTitle);
const cardList = document.createElement('div');
cardList.className = 'card-list';
cardList.dataset.columnId = column.id;
cardList.dataset.swimlaneId = swimlane.id;
// Bu kolon ve swimlane'e ait kartları filtrele
const filteredTasks = column.tasks.filter(
task => task.swimlaneId === swimlane.id
);
filteredTasks.forEach(task => {
const card = createTaskCard(task, swimlane);
cardList.appendChild(card);
});
// Eşit yükseklik için gereken placeholder'ları ekle
const taskCount = filteredTasks.length;
const maxTaskCount = swimlaneSizes[swimlane.id];
// Eğer bu swimlane'de maksimum kart sayısından az kart varsa, placeholder'lar ekle
if (taskCount < maxTaskCount) {
const placeholdersNeeded = maxTaskCount - taskCount;
for (let i = 0; i < placeholdersNeeded; i++) {
const placeholder = document.createElement('div');
placeholder.className = 'card-placeholder';
cardList.appendChild(placeholder);
}
}
swimlaneEl.appendChild(cardList);
columnEl.appendChild(swimlaneEl);
// Sürükle bırak işlemleri
cardList.addEventListener('dragover', handleDragOver);
cardList.addEventListener('drop', handleDrop);
});
kanbanBoard.appendChild(columnEl);
});
setupDragAndDrop();
}
// Kart oluştur
function createTaskCard(task, swimlane) {
const card = document.createElement('div');
card.className = 'card';
card.id = task.id;
card.draggable = true;
card.dataset.taskId = task.id;
const cardTitle = document.createElement('div');
cardTitle.className = 'card-title';
cardTitle.textContent = task.title;
const cardDesc = document.createElement('div');
cardDesc.className = 'card-description';
cardDesc.textContent = task.description;
const cardMeta = document.createElement('div');
cardMeta.className = 'card-metadata';
cardMeta.textContent = task.createdAt;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-card';
deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteTask(task.id);
});
card.appendChild(cardTitle);
card.appendChild(cardDesc);
card.appendChild(cardMeta);
card.appendChild(deleteBtn);
// Kart renklerini swimlane'e göre ayarla
card.style.borderLeft = `3px solid ${swimlane.color}`;
return card;
}
// Select listelerini güncelle
function updateSelects() {
// Kolon select listesini güncelle
taskColumnSelect.innerHTML = '';
state.columns.forEach(column => {
const option = document.createElement('option');
option.value = column.id;
option.textContent = column.title;
taskColumnSelect.appendChild(option);
});
// Swimlane select listesini güncelle
taskSwimlaneSelect.innerHTML = '';
state.swimlanes.forEach(swimlane => {
const option = document.createElement('option');
option.value = swimlane.id;
option.textContent = swimlane.title;
taskSwimlaneSelect.appendChild(option);
});
}
// Kart silme
function deleteTask(taskId) {
state.columns.forEach(column => {
column.tasks = column.tasks.filter(task => task.id !== taskId);
});
saveToLocalStorage();
renderBoard();
}
// Drag & Drop İşlemleri
function setupDragAndDrop() {
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
});
}
let draggedElement = null;
function handleDragStart(e) {
draggedElement = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', e.target.id);
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
draggedElement = null;
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedElement) {
const taskId = draggedElement.dataset.taskId;
const targetColumnId = e.currentTarget.dataset.columnId;
const targetSwimlaneId = e.currentTarget.dataset.swimlaneId;
// Önce tüm kolonlardan kart ı bul ve kaldır
let taskData = null;
state.columns.forEach(column => {
const taskIndex = column.tasks.findIndex(task => task.id === taskId);
if (taskIndex !== -1) {
taskData = column.tasks[taskIndex];
column.tasks.splice(taskIndex, 1);
}
});
// Kart'ı hedef kolona ekle
if (taskData) {
const targetColumn = state.columns.find(col => col.id === targetColumnId);
if (targetColumn) {
taskData.columnId = targetColumnId;
taskData.swimlaneId = targetSwimlaneId;
targetColumn.tasks.push(taskData);
}
}
saveToLocalStorage();
renderBoard();
}
return false;
}
// Dışa Aktarma İşlemi
exportBtn.addEventListener('click', () => {
exportDataTextarea.value = JSON.stringify(state, null, 2);
exportModal.style.display = "block";
});
// Kopyalama butonu
copyExportBtn.addEventListener('click', () => {
exportDataTextarea.select();
document.execCommand('copy');
alert('Veri panoya kopyalandı!');
});
// İçe Aktarma İşlemi
importBtn.addEventListener('click', () => {
importModal.style.display = "block";
});
// İçe aktarma onaylama
confirmImportBtn.addEventListener('click', () => {
try {
const importedData = JSON.parse(importDataTextarea.value.trim());
// Temel doğrulama
if (!importedData.columns || !importedData.swimlanes) {
throw new Error('Geçersiz veri formatı. columns ve swimlanes alanları gerekli.');
}
state = importedData;
saveToLocalStorage();
renderBoard();
updateSelects();
importModal.style.display = "none";
alert('Veriler başarıyla içe aktarıldı!');
} catch (e) {
alert('Veri içe aktarılırken hata oluştu: ' + e.message);
}
});
// Modal kapatma
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', () => {
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = "none";
});
});
});
// Modal dışına tıklayarak kapatma
window.addEventListener('click', (e) => {
document.querySelectorAll('.modal').forEach(modal => {
if (e.target === modal) {
modal.style.display = "none";
}
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment