Created
April 12, 2025 13:30
-
-
Save netologist/d5dc0f916b52701ac9b8089f65df0c4b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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">×</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">×</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 = '×'; | |
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