Created
June 2, 2025 16:36
-
-
Save ORESoftware/ebcd2687189ae8fdfbd186ae1a8b243e to your computer and use it in GitHub Desktop.
kanban board
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="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Kanban Dashboard</title> | |
<style> | |
body { | |
font-family: sans-serif; | |
margin: 0; | |
background-color: #f4f7f6; | |
color: #333; | |
line-height: 1.6; | |
} | |
header { | |
background-color: #333; | |
color: white; | |
padding: 1em; | |
text-align: center; | |
} | |
.kanban-board { | |
display: flex; | |
justify-content: space-around; | |
padding: 20px; | |
gap: 20px; | |
overflow-x: auto; /* Allows horizontal scrolling if columns overflow */ | |
align-items: flex-start; /* Align columns at the top */ | |
} | |
.kanban-column { | |
background-color: #e9ecef; | |
border-radius: 8px; | |
padding: 15px; | |
width: 300px; /* Fixed width for columns */ | |
flex-shrink: 0; /* Prevent columns from shrinking */ | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.kanban-column h2 { | |
text-align: center; | |
margin-top: 0; | |
margin-bottom: 15px; | |
color: #495057; | |
font-size: 1.25em; | |
} | |
.column-content { | |
min-height: 300px; /* To make drop zones visible */ | |
border: 2px dashed transparent; /* For drop highlighting */ | |
padding-bottom: 10px; | |
border-radius: 4px; /* Added for consistency */ | |
} | |
.column-content.drag-over { | |
border-color: #007bff; | |
background-color: #e6f2ff; | |
} | |
.kanban-card { | |
background-color: white; | |
border: 1px solid #ced4da; | |
border-radius: 6px; | |
padding: 12px; | |
margin-bottom: 10px; | |
cursor: grab; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; | |
} | |
.kanban-card.dragging { | |
opacity: 0.5; | |
transform: scale(1.03) rotate(3deg); | |
cursor: grabbing; | |
} | |
.card-thumbnail { | |
width: 100%; | |
max-height: 160px; | |
object-fit: cover; | |
border-radius: 4px; | |
margin-bottom: 10px; | |
} | |
.card-title { | |
font-size: 1.1em; | |
font-weight: bold; | |
margin: 0 0 8px 0; | |
color: #343a40; | |
} | |
.card-author, .card-rating, .card-approvals p { | |
font-size: 0.9em; | |
color: #6c757d; | |
margin: 4px 0; | |
} | |
.card-approvals p { | |
margin-bottom: 5px; /* Spacing for approvals label */ | |
} | |
.card-approvals { | |
margin: 8px 0; | |
} | |
.approval-avatar-container { | |
display: flex; | |
gap: 4px; /* Spacing between avatars */ | |
} | |
.approval-avatar { | |
display: inline-flex; /* Use flex for centering */ | |
align-items: center; | |
justify-content: center; | |
width: 24px; | |
height: 24px; | |
border-radius: 50%; | |
background-color: #adb5bd; /* Default/pending color */ | |
color: white; | |
font-size: 10px; | |
font-weight: bold; | |
box-shadow: 0 0 2px rgba(0,0,0,0.2); | |
} | |
.approval-avatar.pending { background-color: #ffc107; } | |
.approval-avatar.approved { background-color: #28a745; } | |
.approval-avatar.rejected { background-color: #dc3545; } | |
.card-voting { | |
margin-top: 12px; | |
padding-top: 10px; | |
border-top: 1px solid #eee; | |
font-size: 0.9em; | |
} | |
.card-voting label { | |
display: block; | |
margin-bottom: 4px; | |
font-weight: 500; | |
} | |
.card-voting input[type="number"] { | |
width: 80px; | |
padding: 6px; | |
border: 1px solid #ced4da; | |
border-radius: 4px; | |
margin-bottom: 8px; | |
} | |
.card-voting .platform-options label { | |
display: inline-block; /* For horizontal layout of checkboxes */ | |
margin-right: 10px; | |
font-weight: normal; | |
} | |
.card-voting input[type="checkbox"] { | |
margin-right: 4px; | |
vertical-align: middle; | |
} | |
.card-voting p { | |
margin: 8px 0 5px 0; | |
font-weight: 500; | |
} | |
.production-actions-container { /* Wrapper for production actions for a card */ | |
margin-top: 10px; | |
padding: 10px; | |
background-color: #f0f8ff; /* Light blue background */ | |
border: 1px solid #cce5ff; | |
border-radius: 4px; | |
} | |
.production-actions-container h4 { | |
margin-top: 0; | |
margin-bottom: 8px; | |
font-size: 1em; | |
color: #004085; | |
} | |
.production-actions-container button { | |
display: block; | |
width: 100%; | |
padding: 8px 10px; | |
margin-bottom: 6px; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-size: 0.9em; | |
text-align: left; | |
} | |
.production-actions-container button:hover { | |
background-color: #0056b3; | |
} | |
.production-actions-container button:last-child { | |
margin-bottom: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>Content Workflow</h1> | |
</header> | |
<main class="kanban-board"> | |
<div class="kanban-column" id="wip" data-column-id="wip"> | |
<h2>WIP</h2> | |
<div class="column-content" data-column-id="wip-content"> | |
<div class="kanban-card" draggable="true" id="card-1"> | |
<img src="https://via.placeholder.com/280x160/E8117F/FFFFFF?text=ContentA" alt="Content Thumbnail" class="card-thumbnail"> | |
<h3 class="card-title">Amazing New Video</h3> | |
<p class="card-author">Author: Jane Doe</p> | |
<div class="card-approvals"> | |
<p>Approvals:</p> | |
<div class="approval-avatar-container"> | |
<span class="approval-avatar pending" title="User1 (Pending)">U1</span> | |
<span class="approval-avatar approved" title="User2 (Approved)">U2</span> | |
</div> | |
</div> | |
<p class="card-rating">Rating: 7/10</p> | |
</div> | |
</div> | |
</div> | |
<div class="kanban-column" id="staging" data-column-id="staging"> | |
<h2>Staging</h2> | |
<div class="column-content" data-column-id="staging-content"> | |
<div class="kanban-card" draggable="true" id="card-2"> | |
<img src="https://via.placeholder.com/280x160/367588/FFFFFF?text=ContentB" alt="Content Thumbnail" class="card-thumbnail"> | |
<h3 class="card-title">Blog Post Draft</h3> | |
<p class="card-author">Author: John Smith</p> | |
<div class="card-approvals"> | |
<p>Approvals:</p> | |
<div class="approval-avatar-container"> | |
<span class="approval-avatar approved" title="User1 (Approved)">U1</span> | |
<span class="approval-avatar approved" title="User2 (Approved)">U2</span> | |
<span class="approval-avatar rejected" title="User3 (Rejected)">U3</span> | |
</div> | |
</div> | |
<p class="card-rating">Rating: 9/10</p> | |
<div class="card-voting"> | |
<label for="ad-spend-card-2">Ad Spend ($):</label> | |
<input type="number" id="ad-spend-card-2" name="ad-spend" value="100" min="0"> | |
<p>Publish to:</p> | |
<div class="platform-options"> | |
<label><input type="checkbox" name="platform" value="fb"> FB</label> | |
<label><input type="checkbox" name="platform" value="ig" checked> IG</label> | |
<label><input type="checkbox" name="platform" value="tiktok"> TikTok</label> | |
<label><input type="checkbox" name="platform" value="linkedin"> LinkedIn</label> | |
</div> | |
</div> | |
</div> | |
<div class="kanban-card" draggable="true" id="card-3"> | |
<img src="https://via.placeholder.com/280x160/FFC107/000000?text=ContentC" alt="Content Thumbnail" class="card-thumbnail"> | |
<h3 class="card-title">Podcast Episode Idea</h3> | |
<p class="card-author">Author: Alex Green</p> | |
<div class="card-approvals"> | |
<p>Approvals:</p> | |
<div class="approval-avatar-container"> | |
<span class="approval-avatar pending" title="ReviewerA (Pending)">RA</span> | |
<span class="approval-avatar pending" title="ReviewerB (Pending)">RB</span> | |
</div> | |
</div> | |
<p class="card-rating">Rating: (Not Rated)</p> | |
<div class="card-voting"> | |
<label for="ad-spend-card-3">Ad Spend ($):</label> | |
<input type="number" id="ad-spend-card-3" name="ad-spend" value="50" min="0"> | |
<p>Publish to:</p> | |
<div class="platform-options"> | |
<label><input type="checkbox" name="platform" value="fb"> FB</label> | |
<label><input type="checkbox" name="platform" value="x"> X</label> | |
<label><input type="checkbox" name="platform" value="spotify"> Spotify</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="kanban-column" id="production" data-column-id="production"> | |
<h2>Production</h2> | |
<div class="column-content" data-column-id="production-content"> | |
</div> | |
</div> | |
<div class="kanban-column" id="outtake" data-column-id="outtake"> | |
<h2>Outtake</h2> | |
<div class="column-content" data-column-id="outtake-content"> | |
</div> | |
</div> | |
</main> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
const cards = document.querySelectorAll('.kanban-card'); | |
const columns = document.querySelectorAll('.column-content'); | |
let draggedCard = null; | |
let originalColumn = null; | |
// --- Sample Data (In a real app, this would come from a server/API) --- | |
// This data would be more deeply integrated to generate cards dynamically | |
// and persist changes. For now, it's mainly for the production actions. | |
const contentDataStore = { | |
"card-1": { | |
id: "card-1", | |
title: "Amazing New Video", | |
currentColumn: "wip", | |
adSpend: 0, | |
platforms: [] // Will be populated by checkboxes if this card moves to staging | |
}, | |
"card-2": { | |
id: "card-2", | |
title: "Blog Post Draft", | |
currentColumn: "staging", | |
adSpend: 100, // From input | |
platforms: ["ig"] // From checkboxes | |
}, | |
"card-3": { | |
id: "card-3", | |
title: "Podcast Episode Idea", | |
currentColumn: "staging", | |
adSpend: 50, | |
platforms: [] | |
} | |
}; | |
// Initialize or update data based on existing card inputs | |
cards.forEach(card => { | |
const cardId = card.id; | |
if (!contentDataStore[cardId]) contentDataStore[cardId] = { id: cardId, title: card.querySelector('.card-title')?.textContent || 'Untitled', platforms: [], adSpend: 0 }; | |
const adSpendInput = card.querySelector('input[type="number"][name="ad-spend"]'); | |
if (adSpendInput) { | |
contentDataStore[cardId].adSpend = parseInt(adSpendInput.value, 10); | |
adSpendInput.addEventListener('change', (e) => { | |
contentDataStore[cardId].adSpend = parseInt(e.target.value, 10); | |
console.log(`Ad spend for ${cardId} updated to: ${contentDataStore[cardId].adSpend}`); | |
}); | |
} | |
const platformCheckboxes = card.querySelectorAll('input[type="checkbox"][name="platform"]'); | |
platformCheckboxes.forEach(checkbox => { | |
if (checkbox.checked) { | |
if (!contentDataStore[cardId].platforms.includes(checkbox.value)) { | |
contentDataStore[cardId].platforms.push(checkbox.value); | |
} | |
} | |
checkbox.addEventListener('change', (e) => { | |
if (e.target.checked) { | |
if (!contentDataStore[cardId].platforms.includes(e.target.value)) { | |
contentDataStore[cardId].platforms.push(e.target.value); | |
} | |
} else { | |
contentDataStore[cardId].platforms = contentDataStore[cardId].platforms.filter(p => p !== e.target.value); | |
} | |
console.log(`Platforms for ${cardId} updated to:`, contentDataStore[cardId].platforms); | |
}); | |
}); | |
}); | |
// --- Drag and Drop Functionality --- | |
cards.forEach(card => { | |
card.addEventListener('dragstart', (e) => { | |
draggedCard = card; | |
originalColumn = card.parentElement; // Store the original column | |
setTimeout(() => card.classList.add('dragging'), 0); | |
// e.dataTransfer.setData('text/plain', card.id); // Not strictly needed for this same-page example if using draggedCard variable | |
}); | |
card.addEventListener('dragend', () => { | |
if (draggedCard) { // Check if it's still the card being dragged | |
draggedCard.classList.remove('dragging'); | |
} | |
draggedCard = null; | |
originalColumn = null; // Reset original column | |
}); | |
}); | |
columns.forEach(column => { | |
column.addEventListener('dragover', (e) => { | |
e.preventDefault(); // Necessary to allow dropping | |
if (column !== originalColumn) { // Don't highlight if over original column unless empty | |
column.classList.add('drag-over'); | |
} | |
}); | |
column.addEventListener('dragleave', () => { | |
column.classList.remove('drag-over'); | |
}); | |
column.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
column.classList.remove('drag-over'); | |
if (draggedCard) { | |
const targetColumnElement = column.closest('.kanban-column'); | |
const targetColumnId = targetColumnElement.id; | |
const cardId = draggedCard.id; | |
// Append card to the new column | |
column.appendChild(draggedCard); | |
console.log(`Card ${cardId} moved to ${targetColumnId}`); | |
// Update card's column in our data store | |
if (contentDataStore[cardId]) { | |
contentDataStore[cardId].currentColumn = targetColumnId; | |
} | |
// Manage Production Actions | |
removeProductionActions(draggedCard); // Remove from old card if it existed | |
if (targetColumnId === 'production') { | |
addProductionActions(draggedCard); | |
} | |
} | |
}); | |
}); | |
// --- Production Actions Management --- | |
function addProductionActions(cardElement) { | |
const cardId = cardElement.id; | |
const cardData = contentDataStore[cardId]; | |
if (!cardData) return; | |
let actionsContainer = cardElement.querySelector('.production-actions-container'); | |
if (!actionsContainer) { | |
actionsContainer = document.createElement('div'); | |
actionsContainer.className = 'production-actions-container'; | |
cardElement.appendChild(actionsContainer); // Append to the card itself | |
} | |
actionsContainer.innerHTML = ''; // Clear previous content | |
const title = document.createElement('h4'); | |
title.textContent = `Publish: ${cardData.title}`; | |
actionsContainer.appendChild(title); | |
const platformsToPublish = cardData.platforms.length > 0 ? cardData.platforms : ['fb', 'ig', 'tiktok', 'linkedin']; // Default if none explicitly selected | |
platformsToPublish.forEach(platform => { | |
const btn = document.createElement('button'); | |
btn.dataset.platform = platform; | |
// Simple icon mapping for demonstration | |
let icon = ''; | |
if (platform.toLowerCase() === 'fb') icon = 'π '; | |
else if (platform.toLowerCase() === 'ig') icon = 'πΈ '; | |
else if (platform.toLowerCase() === 'tiktok') icon = 'π΅ '; | |
else if (platform.toLowerCase() === 'linkedin') icon = 'π '; | |
else if (platform.toLowerCase() === 'x') icon = 'π¦ '; | |
else if (platform.toLowerCase() === 'spotify') icon = 'π§ '; | |
btn.textContent = `${icon}Connect & Publish to ${platform.toUpperCase()}`; | |
btn.onclick = () => alert(`Initiate publishing to ${platform.toUpperCase()} for "${cardData.title}". Ad Spend: $${cardData.adSpend || 0}`); | |
actionsContainer.appendChild(btn); | |
}); | |
actionsContainer.style.display = 'block'; | |
} | |
function removeProductionActions(cardElement) { | |
const actionsContainer = cardElement.querySelector('.production-actions-container'); | |
if (actionsContainer) { | |
actionsContainer.remove(); | |
} | |
} | |
// Initial check for cards already in production (e.g. if page reloaded with state) | |
document.querySelectorAll('.kanban-column#production .kanban-card').forEach(cardInProduction => { | |
addProductionActions(cardInProduction); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment